Repository navigation
Expand file tree
/
Copy pathstack.zod.ts
More file actions
8164 lines (7785 loc) · 409 KB
/
Copy pathstack.zod.ts
File metadata and controls
8164 lines (7785 loc) · 409 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 { ManifestSchema, resolveInstallModes } from './kernel/manifest.zod';
import { InstalledPackageSourceSchema } from './kernel/package-registry.zod';
import { DEFAULT_METADATA_TYPE_REGISTRY } from './kernel/metadata-plugin.zod';
import {
UNMANAGED_INSTALL_KEY_DISPOSITIONS,
UNMANAGED_INSTALL_REFUSING_DISPOSITIONS,
unmanagedInstallValueCarries,
} from './kernel/unmanaged-install';
import { PLURAL_TO_SINGULAR, pluralToSingular } from './meta-spelling/manifest-collection-spelling';
import { validateObjectNamespacePrefix } from './kernel/namespace-prefix';
import { PLATFORM_CAPABILITY_TOKENS, RETIRED_PLATFORM_CAPABILITY_GUIDANCE } from './kernel/platform-capabilities';
import { DatasourceSchema } from './data/datasource.zod';
import { TranslationBundleSchema, TranslationConfigSchema } from './system/translation.zod';
import { StackServerConfigSchema } from './system/stack-server.zod';
import { StackEmailConfigSchema } from './system/email-config.zod';
import { StackSmsConfigSchema } from './system/sms-config.zod';
import { DevLoginSchema } from './system/dev-login.zod';
import { DemoStaffSchema } from './system/demo-staff.zod';
import { hasPlatformObjectPrefix, isPlatformProvidedObjectName } from './system/constants/platform-object-names';
import { objectStackErrorMap, formatZodError } from './shared/error-map.zod';
import { strictObject } from './shared/strict-object';
import { deepEqualAuthored } from './shared/deep-equal';
import { markStackProvenance, markRefusalConversions, hasStackProvenance, stackConversionsOf } from './stack-provenance';
import {
normalizeStackInput,
MAP_SUPPORTED_FIELDS,
type MetadataCollectionInput,
type MapSupportedField,
} from './shared/metadata-collection.zod';
import type { ConversionNotice } from './conversions/types.js';
import { formatUnknownAuthoringKey } from './data/authoring-key-lint';
import { lintUnknownAuthoringKeys, lintUnknownStackKeys } from './kernel/metadata-authoring-lint';
// Data Protocol
import { ObjectSchema, ObjectExtensionSchema } from './data/object.zod';
import { controlledByParentMasterRefusal } from './data/controlled-by-parent-master';
import { fieldGroupBodyTableRefusal, type NamedInboundRelationship } from './data/related-list-body-table';
import { externalBindingDatasourceRefusal } from './data/external-binding-datasource';
import { InlineGridColumnSchema } from './data/field.zod';
import { AUDIT_FIELD_DEFS } from './data/injected-system-column-provenance';
import { pageComponentSlotPositions } from './ui/component.zod';
import { SeedSchema } from './data/seed.zod';
// UI Protocol
import { AppSchema } from './ui/app.zod';
import { ViewSchema, isAggregatedViewContainer, expandViewContainer } from './ui/view.zod';
import { deriveViewContainerObject } from './ui/view-container-object';
import { defaultRecordPageObject } from './ui/default-record-page';
import { PageSchema } from './ui/page.zod';
import { DashboardSchema } from './ui/dashboard.zod';
import { ReportSchema } from './ui/report.zod';
import { DatasetSchema } from './ui/dataset.zod';
import { ActionSchema, InlineActionSchema } from './ui/action.zod';
// Automation Protocol
import { FlowSchema } from './automation/flow.zod';
import { resolveFlowTriggerKind } from './automation/flow-trigger-kind';
import { FlowFunctionEntrySchema, FlowFunctionEffectSchema, FlowFunctionLoweredDeclarationSchema } from './automation/flow-function.zod';
import { JobSchema } from './system/job.zod';
// Security Protocol
import { PositionSchema } from './identity/position.zod';
import { PermissionSetSchema, findHierarchyScopeGrants } from './security/permission.zod';
import { judgePermissionSetOwnerBinding } from './security/permission-set-owner-binding';
import { judgeCollectionItemOwnerStamp, OWNER_STAMP_JUDGED_COLLECTIONS } from './kernel/collection-item-owner-stamp';
import { CapabilityDeclarationSchema } from './security/capabilities';
import { SharingRuleSchema } from './security/sharing.zod';
import { QueueSchema } from './security/queue.zod';
import { AssignmentRuleSchema } from './automation/assignment-rule.zod';
import { ApiEndpointSchema, type ApiEndpoint } from './api/endpoint.zod';
import {
validateApiEndpointDeclarations,
validateAnonymousElevatedFlows,
type ElevatedFlowCandidate,
} from './api/endpoint-publish-gate';
import { retiredKey } from './shared/retired-key';
// AI Protocol
import { AgentSchema } from './ai/agent.zod';
import { SkillSchema } from './ai/skill.zod';
import { ToolSchema } from './ai/tool.zod';
// Data Protocol (additional)
import { HookSchema } from './data/hook.zod';
import { MappingSchema } from './data/mapping.zod';
import { PicklistSchema, PicklistExtensionSchema } from './data/picklist.zod';
import { CubeSchema } from './data/analytics.zod';
// Automation Protocol (additional)
import { WebhookSchema } from './automation/webhook.zod';
// System Protocol (additional)
import { EmailTemplateDefinitionSchema, EMAIL_TEMPLATE_FLOOR_LOCALE } from './system/email-template.zod';
import { DocSchema } from './system/doc.zod';
import { BookSchema } from './system/book.zod';
// Integration Protocol
import { DeclarativeConnectorEntrySchema } from './integration/connector.zod';
/**
* Datasource Mapping Rule Schema
*
* Defines rules for routing objects to specific datasources based on
* namespace, package, or object name patterns. This provides centralized
* control over datasource assignment without modifying individual objects.
*
* Inspired by Django's Database Router and Kubernetes StorageClass patterns.
*
* @example
* ```ts
* datasourceMapping: [
* { namespace: 'crm', datasource: 'memory' },
* { objectPattern: 'sys_*', datasource: 'turso' },
* { package: 'com.example.analytics', datasource: 'bigquery' },
* { default: true, datasource: 'default' }
* ]
* ```
*/
import { lazySchema } from './shared/lazy-schema';
export const DatasourceMappingRuleSchema = lazySchema(() => z.object({
/**
* Match by namespace (e.g., 'crm', 'auth', 'todo')
* Objects with this namespace will use the specified datasource.
*/
namespace: z.string().optional().describe('Match objects by namespace'),
/**
* Match by package ID (e.g., 'com.example.crm')
* All objects from this package will use the specified datasource.
*/
package: z.string().optional().describe('Match objects by package ID'),
/**
* Match by object name pattern (supports wildcards: *, ?)
* Examples: 'sys_*', 'temp_*', 'cache_*'
*/
objectPattern: z.string().optional().describe('Match objects by name pattern (glob-style)'),
/**
* Mark as default fallback rule.
* This rule applies to all objects that don't match any other rules.
*/
default: z.boolean().optional().describe('Default fallback rule'),
/**
* Target datasource name.
* Must match a registered driver name (e.g., 'memory', 'turso', 'postgres').
*/
datasource: z.string().describe('Target datasource name'),
/**
* Optional priority for rule ordering (lower = higher priority).
* If not specified, rules are evaluated in array order.
*/
priority: z.number().optional().describe('Rule priority (lower = higher priority)'),
}).describe('Datasource routing rule'));
export type DatasourceMappingRule = z.input<typeof DatasourceMappingRuleSchema>;
/**
* Raise every `apis:` publish-gate failure as a Zod issue (#5040 E7).
*
* The #4936 blanket refusal that used to live here — a `.max(0)` on `apis`
* whose error message told the author to delete their endpoints — is GONE, and
* this is what replaced it. Its premise was that nothing executed a declared
* endpoint; the #5040 E-series built the executor (mount seam, matcher, policy
* keys, execution targets, mapping keys), so the refusal would now be the lie
* in the other direction. What survives is the part that was always right:
* a declaration this runtime cannot serve is REFUSED, loudly and with a
* prescription, never parsed into silence.
*
* It hangs off the whole stack object rather than the `apis` field because two
* of the gates are cross-field: the namespace carve-out is derived from
* `manifest.namespace` (ADR-0121 D2), and uniqueness is a property of the set.
* Placing it on the SCHEMA — not inside `defineStack` — is what keeps it
* unavoidable: `defineStack`, `os validate`, the lint scorer, the metadata
* plugin's artifact ingestion and `EnvironmentArtifactSchema.metadata` all run
* through this one parse, so no publish path can forget to check.
*
* [ADR-0138 D2b] The same parse also runs the one cross-item gate: an
* `authRequired: false` endpoint of `type: 'flow'` whose `target` names a flow
* of this stack declaring `runAs: 'system'` is refused, located at the
* endpoint's `authRequired`, with the record's prescription. Both items are in
* this one input, so the refusal holds whichever of the two was edited last. A
* target flow outside the stack is not readable here; the metadata layer's
* doors read it there (`validateAnonymousElevatedFlows`' module notes).
*/
function applyApiEndpointGates(
config: {
manifest?: { namespace?: string | undefined } | undefined;
apis?: ApiEndpoint[] | undefined;
flows?: readonly ElevatedFlowCandidate[] | undefined;
},
ctx: z.RefinementCtx,
): void {
for (const issue of validateAnonymousElevatedFlows(config.apis, config.flows)) {
ctx.addIssue({ code: 'custom', path: issue.path, message: issue.message });
}
for (const issue of validateApiEndpointDeclarations(config.apis, {
namespace: config.manifest?.namespace,
})) {
ctx.addIssue({ code: 'custom', path: issue.path, message: issue.message });
}
}
/**
* [ADR-0086 D3] Raise every permission set whose body `packageId` disagrees
* with the package that ships it as a Zod issue at that key: the BUILD door of
* the owner-binding judge ({@link judgePermissionSetOwnerBinding},
* `security/permission-set-owner-binding.ts`, whose header holds the rule and
* every other door). A set's owner is the package it is registered under, so a
* key naming another package (or any key, on a set no package ships) was
* registered over in silence; refused here instead, before anything is built.
*
* On the SCHEMA rather than inside `defineStack`, for the reason
* {@link applyApiEndpointGates} gives: `defineStack`, `os validate`, `os build`,
* the lint scorer and the metadata plugin's artifact ingestion all run this one
* parse, so no build or load path can skip the judgement. The binding is the id
* each set is registered under, and nothing else:
*
* - **no `packages[]`**: the stack IS its one package, so every top-level set
* ships from `manifest.id`, and from no package when there is no `manifest`;
* - **`packages[]`** (ADR-0130 D4): a body's sets ship from that body's own
* `id`. A top-level set that no body declares is the stack's residual,
* registered under `manifest.id` (the residual rule is `unclaimedTopLevel` in
* `@objectstack/metadata`; a permission set's slot there is its name). A
* top-level set that a body declares is a copy of that body's set and is never
* registered from the top level, so the top level does not judge it.
* The residual rule has one exception: a residual PICKLIST is kept as
* metadata only, never registered with the engine. When a residual object's
* field names it, the boot lists that object and does not serve it. When a
* package body's field names it, the boot's `kernel:ready` picklist audit
* refuses that field, and `os validate` / `os build` report it as
* `picklist-reference-unknown`. When no object field the engine registers
* names it, the boot refuses the stack, and `os validate` / `os build`
* refuse it first.
*/
function applyPermissionSetOwnerBindingGate(
config: { manifest?: { id?: unknown } | undefined; permissions?: unknown; packages?: unknown },
ctx: z.RefinementCtx,
): void {
const nameOf = (set: unknown): unknown => (set as { name?: unknown } | null | undefined)?.name;
const judge = (set: unknown, binding: unknown, path: (string | number)[]): void => {
const name = nameOf(set);
const refusal = judgePermissionSetOwnerBinding(
set,
typeof name === 'string' ? name : String(name),
typeof binding === 'string' ? binding : undefined,
'build',
);
if (refusal) ctx.addIssue({ code: 'custom', path: [...path, 'packageId'], message: refusal.message });
};
const topLevel = Array.isArray(config.permissions) ? config.permissions : [];
if (!Array.isArray(config.packages)) {
topLevel.forEach((set, i) => judge(set, config.manifest?.id, ['permissions', i]));
return;
}
const claimed = new Set<string>();
config.packages.forEach((entry: unknown, j: number) => {
const body = (entry as { manifest?: { id?: unknown; permissions?: unknown } } | null | undefined)?.manifest;
if (!body || !Array.isArray(body.permissions)) return;
body.permissions.forEach((set: unknown, i: number) => {
const name = nameOf(set);
if (typeof name === 'string') claimed.add(name);
judge(set, body.id, ['packages', j, 'manifest', 'permissions', i]);
});
});
topLevel.forEach((set, i) => {
const name = nameOf(set);
if (typeof name === 'string' && claimed.has(name)) return;
judge(set, config.manifest?.id, ['permissions', i]);
});
}
/**
* [ADR-0010] Raise every collection item whose body carries a `_packageId`
* other than the package that ships it as a Zod issue at that key: the BUILD
* door of the owner-stamp judge ({@link judgeCollectionItemOwnerStamp},
* `kernel/collection-item-owner-stamp.ts`, whose header holds the rule, the
* collections and the other door). The registration door refuses the same
* stamp at boot or install; judged here too, an author hears it where they
* wrote it.
*
* On the SCHEMA rather than inside `defineStack`, for the reason
* {@link applyApiEndpointGates} gives: `defineStack`, `os validate`, `os build`,
* the lint scorer and the metadata plugin's artifact ingestion all run this one
* parse. It reads the PARSED stack, and every item schema of a judged
* collection declares `_packageId` (`MetadataProtectionFields`), so the parse
* keeps the key for the judge. The owner is the id each item is registered
* under, read the way {@link applyPermissionSetOwnerBindingGate} reads a
* permission set's:
*
* - **no `packages[]`**: every top-level item ships from `manifest.id`, and
* from no package when there is no `manifest`;
* - **`packages[]`** (ADR-0130 D4): a body's items ship from that body's own
* `id`. The top level is judged as the boot's residual rule reads it
* (`unclaimedTopLevel`, `@objectstack/metadata`), mirrored by
* {@link isResidualTopLevelItem} because this package cannot import that one:
* a top-level item is the stack's residual, registered under `manifest.id`
* and judged against it, only when it occupies a registry slot no body
* occupies. A top-level item a body declares is a copy of that body's item,
* never registered from the top level, so the top level does not judge it.
*
* A stack's `plugins` are runtime plugin instances (`z.unknown()`), not bodies
* this parse can read; the registration door judges a nested plugin's items
* when it registers them.
*/
function applyCollectionItemOwnerStampGate(
config: Record<string, unknown> & { manifest?: { id?: unknown } | undefined; packages?: unknown },
ctx: z.RefinementCtx,
): void {
const nameOf = (key: string, item: unknown): string => {
const it = (item ?? {}) as { name?: unknown; id?: unknown; object?: unknown };
const name = key === 'data' ? it.object : (it.name ?? it.id ?? (key === 'views' ? it.object : undefined));
return typeof name === 'string' && name !== '' ? name : '(unnamed)';
};
const judgeBody = (
body: Record<string, unknown>,
owner: unknown,
path: (string | number)[],
judged?: (key: string, item: unknown) => boolean,
): void => {
for (const key of OWNER_STAMP_JUDGED_COLLECTIONS) {
const items = body[key];
if (!Array.isArray(items)) continue;
items.forEach((item: unknown, i: number) => {
if (judged && !judged(key, item)) return;
const name = nameOf(key, item);
const refusal = judgeCollectionItemOwnerStamp(
item,
pluralToSingular(key),
name,
typeof owner === 'string' ? owner : undefined,
'build',
);
if (refusal) ctx.addIssue({ code: 'custom', path: [...path, key, i, '_packageId'], message: refusal.message });
});
}
};
if (!Array.isArray(config.packages)) {
judgeBody(config, config.manifest?.id, []);
return;
}
const bodies: Record<string, unknown>[] = [];
config.packages.forEach((entry: unknown, j: number) => {
const body = (entry as { manifest?: Record<string, unknown> } | null | undefined)?.manifest;
if (!body || typeof body !== 'object') return;
bodies.push(body);
judgeBody(body, body.id, ['packages', j, 'manifest']);
});
judgeBody(config, config.manifest?.id, [], isResidualTopLevelItem(bodies));
}
/**
* The judged collections a `packages[]` stack's TOP LEVEL never files as
* residual items: the boot's residual rule (`unclaimedTopLevel`,
* `@objectstack/metadata`) walks the collections the metadata door registers
* as items (its `ARTIFACT_FIELD_TO_TYPE`), and these two are not among them,
* so the boot judges no stamp on a top-level item of either. Both are read off
* the stack whole, the top level merged with every body
* (`resolveArtifactCollections`, `@objectstack/runtime`): `AppPlugin` hands the
* seed `data` to the seed loader and schedules the `jobs`.
* `datasets` is not here: the residual rule walks it, so a top-level dataset
* that no body declares is registered under `manifest.id` and its stamp is
* judged at both doors, like every other residual item.
* A package BODY's items of these collections are still judged, at both doors.
*
* Mirrored, because `@objectstack/spec` cannot import the rule it mirrors; a
* `@objectstack/metadata` test holds the two equal, collection by collection.
*/
const TOP_LEVEL_COLLECTIONS_NEVER_RESIDUAL: ReadonlySet<string> = new Set(['data', 'jobs']);
/**
* The registry slot one item occupies, the way the boot's residual rule
* derives it (`artifactItemSlot`, `@objectstack/metadata`): an aggregated view
* container is filed under the object it binds to
* ({@link deriveViewContainerObject}), however that binding is spelled; any
* other item under its `name`, a `views` entry without one falling back to the
* same derivation. `undefined`: no slot, so no door registers the item.
*/
function residualSlotOf(key: string, item: unknown): string | undefined {
if (key === 'views' && isAggregatedViewContainer(item)) return deriveViewContainerObject(item) || undefined;
const name = (item as { name?: unknown } | null | undefined)?.name;
if (name) return String(name);
return key === 'views' ? deriveViewContainerObject(item) || undefined : undefined;
}
/**
* Whether a top-level item of a `packages[]` stack is its RESIDUAL: the
* mirror of `unclaimedTopLevel`'s judgement (`@objectstack/metadata`). True
* when the item's collection is one the rule walks
* ({@link TOP_LEVEL_COLLECTIONS_NEVER_RESIDUAL}), the item occupies a slot
* ({@link residualSlotOf}), and no body occupies that slot in the same
* collection: a body's container claims its object's slot and the slot of
* every view it expands into ({@link expandViewContainer}), any other body
* item its name's.
*/
function isResidualTopLevelItem(bodies: readonly Record<string, unknown>[]): (key: string, item: unknown) => boolean {
// `<collection>:<slot>`: no collection key carries a `:`, so the pair is unambiguous.
const claimed = new Set<string>();
for (const body of bodies) {
for (const key of OWNER_STAMP_JUDGED_COLLECTIONS) {
const items = body[key];
if (!Array.isArray(items)) continue;
for (const item of items) {
const slot = residualSlotOf(key, item);
if (slot === undefined) continue;
claimed.add(`${key}:${slot}`);
if (key === 'views' && isAggregatedViewContainer(item)) {
for (const view of expandViewContainer(slot, item)) claimed.add(`views:${view.name}`);
}
}
}
}
return (key, item) => {
if (TOP_LEVEL_COLLECTIONS_NEVER_RESIDUAL.has(key)) return false;
const slot = residualSlotOf(key, item);
return slot !== undefined && !claimed.has(`${key}:${slot}`);
};
}
/** One `objectExtensions` entry as the registry registers it, with where it sits in the stack. */
interface VisibleObjectExtension {
/** The position of the package that ships it: registration order across packages. */
readonly carrier: number;
/** The entry's path in the stack, e.g. `['objectExtensions', 0]`. */
readonly path: readonly (string | number)[];
/** `ext.priority ?? 200`, the registry's own default (`engine.ts`, the `objectExtensions` loop). */
readonly priority: number;
readonly fields: Record<string, unknown>;
}
/**
* [#23210 · ADR-0055 §1] The controlled-by-parent master rule, judged on the
* MERGED field set of an object that an `objectExtensions` entry of this stack
* extends — the BUILD door of the rule the per-object refinement
* (`refuseControlledByParentWithoutOneMaster`, `data/object.zod.ts`) applies
* to an object as it is declared.
*
* ## Why the per-object refinement is not enough
*
* The registry folds every extension onto its target at boot
* (`SchemaRegistry.resolveObject`, `objectql/src/registry.ts`), so an object
* that passes as declared can run with two required `master_detail` fields —
* or none — once an extension in the same stack is merged. The runtime then
* derives no master and fails closed (every read denied, every write refused),
* while `defineStack` and `os validate` had said nothing. The same merged
* object declared directly is refused here, with this message; so is the
* extension that produces it.
*
* ## The merge, read exactly as the registry computes it
*
* - The base is the target's own declaration; an extension carries no
* `sharingModel` (`ObjectExtensionSchema` is closed and the merge copies
* `fields`, `validations`, `indexes` and three labels only), so the merged
* object's `sharingModel` is the target's.
* - `fields` merge BY NAME, a later writer replacing the whole field
* (`mergeObjectDefinitions`: `{ ...base.fields, ...extension.fields }`). An
* extension can therefore demote the one master (`required: false`, another
* type) as well as add a second.
* - Extensions apply in ascending `priority`, equal priorities in
* registration order (a stable sort), and a package registers one extension
* per target: a later entry of the same package replaces an earlier one.
*
* ## Which targets this door can see — and the ones it leaves unjudged
*
* The registry keys an extension by its `extend` string verbatim
* (`computeFQN` is the identity), so an extension merges into the object whose
* `name` is exactly that string, and only that one. A target is visible when
* the stack declares it:
*
* - **no `packages[]`**: the stack is its one package — its top-level
* `objects`, extended by its top-level `objectExtensions`;
* - **`packages[]`** (ADR-0130 D4): the artifact is the co-ownership
* boundary, so an extension in any body judges a target declared in any
* body, or a top-level object no body declares (the residual the engine
* registers too). The top-level `objectExtensions` are not read there: the
* engine registers a multi-package stack's bodies and the residual's
* objects, never its top-level extensions.
*
* Left unjudged, on purpose, so that nothing here guesses at an object the
* stack cannot see: an `extend` naming no object of the stack (another
* package's, the platform's); a target two bodies declare (the registry
* refuses that ownership conflict on its own); a target whose own declaration
* already breaks the rule (the per-object refinement reports it there); and a
* target on which two packages write one field key at the `priority` that
* decides it, whose winner is the registration order the artifact loader
* computes (`resolveArtifactPackageOrder`, dependency-topological), not this
* parse.
*
* ## Where the issue sits
*
* At the `fields` of each extension that wrote the field that decides the
* verdict: the last writer of every key that is a `master_detail` in the
* target's declaration or in the merged set. That is the line that introduced
* the second master, or that demoted the one. The message is the per-object
* refinement's own ({@link controlledByParentMasterRefusal}), naming the
* target and the merged fields — one rule, one wording.
*
* On the SCHEMA, for the reason {@link applyApiEndpointGates} gives:
* `defineStack`, `os validate`, `os build`, the lint scorer and an artifact
* load all run this parse.
*/
function applyControlledByParentExtensionGate(
config: { objects?: unknown; objectExtensions?: unknown; packages?: unknown },
ctx: z.RefinementCtx,
): void {
const asRecord = (value: unknown): Record<string, unknown> | undefined =>
typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
const bodies = Array.isArray(config.packages)
? config.packages.map((entry: unknown) => asRecord(asRecord(entry)?.manifest) ?? {})
: undefined;
// Target name → its declaration, or `null` when two packages claim it.
const targets = new Map<string, Record<string, unknown> | null>();
const declare = (objects: unknown, carrier: number, owners: Map<string, number>): void => {
if (!Array.isArray(objects)) return;
for (const obj of objects) {
const record = asRecord(obj);
const name = record?.name;
if (!record || typeof name !== 'string' || name.length === 0) continue;
const owner = owners.get(name);
if (targets.get(name) === null) continue;
if (owner !== undefined && owner !== carrier) {
targets.set(name, null);
continue;
}
owners.set(name, carrier);
// The same package declaring one name twice re-registers it: the last wins.
targets.set(name, record);
}
};
const owners = new Map<string, number>();
if (bodies === undefined) {
declare(config.objects, 0, owners);
} else {
bodies.forEach((body, j) => declare(body.objects, j, owners));
// The residual: a top-level object no body declares.
const residual = Array.isArray(config.objects)
? config.objects.filter((obj: unknown) => {
const name = asRecord(obj)?.name;
return typeof name === 'string' && !owners.has(name);
})
: [];
declare(residual, bodies.length, owners);
}
// Target name → its extensions, one per package (the last entry wins).
const extensions = new Map<string, Map<number, VisibleObjectExtension>>();
const collect = (entries: unknown, carrier: number, at: readonly (string | number)[]): void => {
if (!Array.isArray(entries)) return;
entries.forEach((entry: unknown, i: number) => {
const record = asRecord(entry);
const target = record?.extend;
if (!record || typeof target !== 'string' || !targets.get(target)) return;
const byCarrier = extensions.get(target) ?? new Map<number, VisibleObjectExtension>();
byCarrier.set(carrier, {
carrier,
path: [...at, 'objectExtensions', i],
priority: typeof record.priority === 'number' ? record.priority : 200,
fields: asRecord(record.fields) ?? {},
});
extensions.set(target, byCarrier);
});
};
if (bodies === undefined) collect(config.objectExtensions, 0, []);
else bodies.forEach((body, j) => collect(body.objectExtensions, j, ['packages', j, 'manifest']));
for (const [target, byCarrier] of extensions) {
const base = targets.get(target);
const baseFields = asRecord(base?.fields);
if (!base || !baseFields || base.sharingModel !== 'controlled_by_parent') continue;
if (controlledByParentMasterRefusal(target, base.sharingModel, baseFields) !== undefined) continue;
const ordered = [...byCarrier.values()].sort((a, b) => a.priority - b.priority || a.carrier - b.carrier);
const writers = new Map<string, VisibleObjectExtension>();
// Per key, how many packages wrote it at the priority of its last writer.
// Above one, the winner is registration order, which this parse does not compute.
const tiedWriters = new Map<string, number>();
const merged: Record<string, unknown> = { ...baseFields };
for (const extension of ordered) {
for (const [key, field] of Object.entries(extension.fields)) {
const previous = writers.get(key);
tiedWriters.set(key, previous?.priority === extension.priority ? (tiedWriters.get(key) ?? 1) + 1 : 1);
writers.set(key, extension);
merged[key] = field;
}
}
if ([...tiedWriters.values()].some((count) => count > 1)) continue;
const refusal = controlledByParentMasterRefusal(target, base.sharingModel, merged);
if (refusal === undefined) continue;
const isMaster = (field: unknown): boolean => asRecord(field)?.type === 'master_detail';
const blamed = new Set<VisibleObjectExtension>();
for (const [key, extension] of writers) {
if (isMaster(merged[key]) || isMaster(baseFields[key])) blamed.add(extension);
}
for (const extension of ordered) {
if (!blamed.has(extension)) continue;
ctx.addIssue({ code: 'custom', path: [...extension.path, 'fields'], message: refusal.message });
}
}
}
/**
* One package entry as an AUTHOR writes it — manifest only (ADR-0130 D4).
*
* A release artifact MAY carry N package manifests: everything inside one
* artifact is delivered atomically by one publisher, and that joint delivery IS
* the co-ownership declaration (ADR-0130 D1).
*
* ## This is the AUTHORING stage, not the artifact's element type
*
* The element type of `packages` on {@link ObjectStackDefinitionSchema} is
* {@link ArtifactPackageSchema}, whose body half is the ASSEMBLED package body
* — manifest fields plus the metadata collections that package owns, which is
* what `ObjectQL.registerApp` iterates. This schema stays at the stage its body
* half can actually describe: `ManifestSchema`, whose `objects` are glob
* PATTERNS. #14242 measured that a full parse of a real artifact entry against
* it is refused (`manifest.objects.0: expected string, received object`), and
* the maintainer's decision (2026-09-02, road B) was to declare the assembled
* stage beside this one rather than widen either into a union that could check
* neither.
*
* So: this schema for what an author hands `composeStacks`,
* {@link ArtifactPackageSchema} for what the artifact carries and the load path
* registers. An authoring entry is a valid instance of the assembled one
* whenever its manifest declares no globs — a package identity with no
* collections.
*
* ## ⛔ The entry is a WRAPPER object, and that is the whole point of its shape
*
* The manifest body sits UNDER a key (`manifest`); it is never inlined flat as
* the array element itself. ADR-0130 D4 reserves the position deliberately, at
* schema time, and says why: an artifact schema is on disk at every customer,
* so format is the hardest decision to revisit — 黑猫's 30-object artifact is
* already 2.6 MB, and modules accreting into one JSON grow both marketplace
* transfer and startup parse. When a future external-segment form lands, it is
* `{ ref, integrity }` — an ADDITIVE key on this existing object. Flatten the
* manifest into the array element instead and that same future is a SHAPE
* change: `ref`/`integrity` would have to be bolted onto `ManifestSchema`
* (which every other consumer shares) and every required manifest field would
* have to go optional, because a segment reference carries no manifest content
* at all.
*
* ⛔ Segmented loading itself is NOT implemented and is an explicit Non-goal of
* ADR-0130 ("D4 reserves the key position only. The segmented form itself needs
* its own decision."). This schema reserves structure, nothing else.
*
* Forward compatibility rides the mechanism that already exists —
* `manifest.engines.protocol` (ADR-0025, `kernel/manifest.zod.ts`). A
* new-format artifact declares a new protocol range and an older runtime
* refuses it cleanly instead of mis-parsing it into a half-registered install.
* ⛔ No new version-negotiation mechanism is introduced here.
*
* @example
* ```jsonc
* {
* "packages": [
* { "manifest": { "id": "com.example.crm", "name": "crm", "version": "1.0.0",
* "type": "app", "namespace": "crm" } },
* { "manifest": { "id": "com.example.crm.cpq", "name": "cpq", "version": "1.0.0",
* "type": "module", "namespace": "crm" } }
* ]
* }
* ```
*/
export const ArtifactPackageEntrySchema = lazySchema(() => strictObject({
surface: 'an artifact package entry',
history:
'The entry is a wrapper object whose manifest body lives under `manifest:` — the '
+ 'structural position ADR-0130 D4 reserves so a future `{ ref, integrity }` external '
+ 'segment is an additive key rather than a reshape. An inlined manifest body (`id`, '
+ '`name`, `version`, … written directly on the array element) is therefore refused: '
+ 'wrap it as `{ manifest: { … } }`.',
}, {
manifest: ManifestSchema.describe('The package manifest this artifact entry carries'),
}).describe('One package entry as authored — manifest only (ADR-0130 D4)'));
export type ArtifactPackageEntry = z.input<typeof ArtifactPackageEntrySchema>;
/** Post-parse shape of {@link ArtifactPackageEntry} — defaults applied, transforms run (ADR-0122). */
export type ArtifactPackageEntryParsed = z.infer<typeof ArtifactPackageEntrySchema>;
/**
* The datasource name the HOST owns (#3826, #23634): the runtime declares and
* connects `default` itself (`DefaultDatasourcePlugin`, from `OS_DATABASE_URL`
* or the dev fallback), and `AppPlugin` refuses a stack that declares it in
* every boot posture — with a URL, with `--dev`, and on the standalone path.
* The comparison is the runtime's: the entry's `name` equals `default` exactly
* (the map form arrives here already normalized, key → `name`; every other
* spelling of the word is already refused by `DatasourceSchema.name`'s
* snake_case pattern).
*
* The refusal lives on the STACK's collection, ⛔ never on
* {@link DatasourceSchema}: that schema also documents the host's own
* `default` row (the registry, the admin list, the metadata door), which must
* keep parsing. Saying it here moves the runtime's refusal to the authoring
* doors — `defineStack`, `os validate` and publish — with the same sentence
* and the remedy, instead of after the boot's earlier plugins have started.
*/
const HOST_RESERVED_DATASOURCE_NAME = 'default';
function refuseHostReservedDatasourceName(datasources: readonly { name?: unknown }[], ctx: z.RefinementCtx): void {
datasources.forEach((datasource, index) => {
if (datasource?.name !== HOST_RESERVED_DATASOURCE_NAME) return;
ctx.addIssue({
code: 'custom',
path: [index, 'name'],
message:
`This stack declares a datasource named '${HOST_RESERVED_DATASOURCE_NAME}' — that name is reserved for ` +
`the host's primary datasource, and every boot refuses a stack that declares it. To give the project ` +
`its own primary database, set OS_DATABASE_URL and omit this entry. To add a datasource of the stack's ` +
`own, declare it under another name (e.g. 'app_primary') and route objects to it explicitly (an ` +
`object's \`datasource\`, or a \`datasourceMapping\` rule).`,
});
});
}
/**
* Every metadata COLLECTION `ObjectStackDefinitionSchema` declares, as one
* named shape.
*
* ## Why the collections are a const rather than written inline
*
* Two surfaces need this exact key set and must not be able to disagree about
* it: the stack definition (spread in below) and {@link
* AssembledPackageBodySchema}, the assembled form of ONE package inside a
* release artifact (ADR-0130 D4). Declaring the collections twice is the drift
* ADR-0116 exists about, and here it would be silent in the worst direction —
* a metadata family added to the stack and forgotten on the package body would
* make the load gate refuse every multi-package artifact that uses it, naming a
* key its author correctly wrote.
*
* ⛔ The artifact ENVELOPE keys stay on the schema below and are deliberately
* NOT in here: `manifest`, `packages`, `api`, `server`, `i18n`, `runtimeModule`
* and `onEnable` describe the artifact or the deployment, not one package
* inside it. `packages` additionally CANNOT live here — its element schema is
* built from this shape, so putting it in the shape would make the declaration
* circular.
*
* Three members of this shape are envelope keys as well: `plugins`,
* `devPlugins` and `devLogins` are stack collections (they compose by
* `concat`, so they live here), but they describe the BOOT rather than a
* package's metadata — the first two are runtime ASSEMBLY instructions, the
* third is what the development banner prints — and
* {@link ASSEMBLED_PACKAGE_BODY_ENVELOPE_KEYS} keeps all three out of the
* assembled package body (#15219, #17556 — {@link AssembledPackageBodyKey}
* says why).
*
* @internal
*/
const STACK_DEFINITION_COLLECTIONS_SHAPE = {
datasources: z.array(DatasourceSchema)
.superRefine(refuseHostReservedDatasourceName)
.optional()
.describe('External Data Connections'),
/**
* Datasource Mapping Configuration
*
* Centralized routing rules that map packages, namespaces, or object patterns
* to specific datasources. This eliminates the need to configure datasource
* on every individual object.
*
* Rules are evaluated in order (or by priority if specified). First match wins.
* If no match, falls back to object's explicit `datasource` field, then 'default'.
*
* @example
* ```ts
* datasourceMapping: [
* // System objects use Turso (persistent storage)
* { objectPattern: 'sys_*', datasource: 'turso' },
* { namespace: 'auth', datasource: 'turso' },
*
* // CRM application uses Memory (dev/test)
* { namespace: 'crm', datasource: 'memory' },
* { package: 'com.example.crm', datasource: 'memory' },
*
* // Temporary objects use Memory
* { objectPattern: 'temp_*', datasource: 'memory' },
*
* // Default fallback
* { default: true, datasource: 'turso' },
* ]
* ```
*/
datasourceMapping: z.array(DatasourceMappingRuleSchema).optional()
.describe('Centralized datasource routing rules for packages/namespaces/objects'),
translations: z.array(TranslationBundleSchema).optional().describe('I18n Translation Bundles'),
i18n: TranslationConfigSchema.optional().describe('Internationalization configuration'),
/**
* ObjectQL: Data Layer
* All business objects and entities.
*/
objects: z.array(ObjectSchema).optional().describe('Business Objects definition (owned by this package)'),
/**
* Object Extensions: fields/config to merge into objects owned by other packages.
* Use this instead of redefining an object when you want to add fields to
* an existing object from another package.
*
* @example
* ```ts
* objectExtensions: [{
* extend: 'contact',
* fields: { sales_stage: Field.select([...]) },
* }]
* ```
*/
objectExtensions: z.array(ObjectExtensionSchema).optional().describe('Extensions to objects owned by other packages'),
/**
* Shared option lists (`data/picklist.zod.ts`): one list of select options
* that fields on any object reference by name — `Field.select({ picklist:
* 'industry' })` — instead of each field copying an `options` array.
*/
picklists: z.array(PicklistSchema).optional().describe('Shared option lists that select fields reference by name'),
/**
* Picklist Extensions: options to ADD to picklists owned by other packages —
* the `objectExtensions` idiom, additive only.
*
* @example
* ```ts
* picklistExtensions: [{
* extend: 'industry',
* options: [{ label: 'Healthcare', value: 'healthcare' }],
* }]
* ```
*/
picklistExtensions: z.array(PicklistExtensionSchema).optional().describe('Options added to picklists owned by other packages (additive only)'),
/**
* ObjectUI: User Interface Layer
* Apps, Menus, Pages, and Visualizations.
*/
apps: z.array(AppSchema).optional().describe('Applications'),
// #3464: the top-level `portals` collection was removed — PortalSchema was a
// never-enforced, no-op projection (no dispatcher route family, auth scope,
// LayoutDispatcher, NavigationBuilder or ThemeProvider ever consumed it).
// Author external-user UI with apps/views + positions & permission sets.
views: z.array(ViewSchema).optional().describe('List Views'),
/**
* [#5320] MACHINE-ASSEMBLED ONLY — not an authoring surface.
*
* Runtime-assembled manifests (`GET /packages/:id/export`, environment
* artifact bundles) carry their non-container view artifacts — tenant-
* authored standalone ViewItems, flattened overlays, expanded items a
* travelling container cannot re-derive — under this key, judged by
* `AssembledViewArtifactSchema` (`ui/assembled-views.zod.ts`). Authored
* stack sources never write it: views are authored as `defineView`
* containers in `views:`, and a standalone view is authored through the
* metadata door (Studio / `PUT /api/v1/meta/view`), not in stack source.
*
* Declared here as an always-refusing key — the `retiredKey()` mechanism,
* applied to a key that is machine-only rather than removed — for the same
* reason the tombstones exist (#3855): this schema is not `.strict()`, so an
* UNDECLARED `viewItems:` would be silently stripped and the author would
* learn nothing. Declared-refusing, the mistake fails `tsc` (input type
* `never`) and the parse refusal carries the prescription itself.
*/
viewItems: z.never({
error: () =>
'`viewItems` is the machine-assembled channel for non-container view artifacts in '
+ 'runtime-assembled manifests (package export, environment artifacts) — it is not an '
+ 'authoring surface. Author views as `defineView` containers in `views:`; author a '
+ 'standalone view through the metadata door (Studio / `PUT /api/v1/meta/view`) instead '
+ 'of stack source.',
}).optional().describe(
'[MACHINE-ASSEMBLED] Non-container view artifacts of a runtime-assembled manifest '
+ '(standalone ViewItems, flattened overlays) — written by package export and artifact '
+ 'factories, refused in authored stack sources.',
),
pages: z.array(PageSchema).optional().describe('Custom Pages'),
dashboards: z.array(DashboardSchema).optional().describe('Dashboards'),
reports: z.array(ReportSchema).optional().describe('Analytics Reports'),
datasets: z.array(DatasetSchema).optional().describe('Analytics semantic-layer datasets (ADR-0021)'),
/**
* Registered actions — global (no `objectName`) and object-bound (`objectName`
* set; `defineStack` merges those into the named object's `actions`).
*
* Uniqueness is per SCOPE, not per stack. The runtime registers and dispatches
* every action under one exact-string key — the owning object's name (or
* `'global'` for an object-less action), a colon, then the action name — with
* no wildcard semantics, so `defineStack` refuses two declarations that
* resolve to the same key: both written here, both on one object's `actions`,
* or one in each position, byte-identical twins included (that case is a
* delete, not a rename). An embedded action is keyed by the object it is
* written ON — the declaration-resolution key `collectActionDeclarations` /
* `resolveRouteActionDeclaration` use — not by its own `objectName` (the
* registration key `collectBundleActions` / `standaloneActionOwnerKey` read).
* One
* global and one object-bound action MAY share a `name`: they occupy two
* keys, and a by-name reader on the object's route resolves the object's own
* `actions` (embedded, or merged in from here) before a standalone global
* declaration.
*/
actions: z.array(ActionSchema).optional().describe(
'Global and Object Actions. Unique per scope, not per stack: the runtime keys every action by '
+ "its owning object's name (or 'global' when object-less), a colon, then the action name, and "
+ 'defineStack refuses two declarations that resolve to one key — both here, both on one '
+ "object's actions, or one in each position, identical twins included (an embedded action is keyed "
+ 'by the object it is written on, not by its own objectName). '
+ "One global and one object-bound action may share "
+ "a name; on that object's route the object's own actions take precedence for by-name readers. "
+ 'composeStacks runs the same key rule across its input stacks (counting distinct stacks, not sites) '
+ 'and names both source stacks on a collision.',
),
// `themes` was REMOVED in 17.1 (commit 35ad101bc, ADR-0049 enforce-or-remove — ruled
// 退役授权面, 2026-08-21). The pipeline was live from authoring gate through
// artifact ingest and stopped there: no framework package ever read the
// stored items, `theme` was never a registered metadata type, no first-party
// app mounted the spec-aware provider, and nothing selected a theme — an
// authored theme shipped green and changed nothing. `app.branding` is the
// one colour surface. The unknown-key rejection carries the prescription via
// the `guidance` entry on this schema's `strictObject` options; the D3
// semantic entry `stack-themes-carrier-retired` carries it to upgraders.
/**
* ObjectFlow: Automation Layer
* Business logic, approvals, and flows.
*
* ADR-0019: approvals are no longer a top-level collection — an approval is
* authored as a flow with one or more Approval nodes, so it lives in `flows`.
* ADR-0020: there is no top-level `workflows` collection — record state
* machines are a `state_machine` validation rule on each object.
*/
flows: z.array(FlowSchema).optional().describe('Screen Flows'),
jobs: z.array(JobSchema).optional().describe('Background / Scheduled Jobs (run by IJobService on cron/interval/once schedules)'),
emailTemplates: z.array(EmailTemplateDefinitionSchema).optional().describe('Email Templates resolved by IEmailService.sendTemplate({ template, locale })'),
docs: z.array(DocSchema).optional().describe('Package documentation — flat Markdown items compiled from src/docs/*.md (ADR-0046)'),
books: z.array(BookSchema).optional().describe('Documentation navigation spines — ordered groups with derived membership (ADR-0046 §6)'),
/**
* ObjectGuard: Security Layer
*/
positions: z.array(PositionSchema).optional().describe('Positions — flat capability-distribution groups (ADR-0090 D3)'),
/**
* Permission Sets — the ADR-0090 app-category collection.
*
* ⚠️ **`permissions` is one key with two incompatible readings**, and this is
* the collection half. The other half is
* `ManifestSchema.permissions` (`kernel/manifest.zod.ts`): at the AUTHORING
* stage the same key is the ADR-0025 §3.2 capability GRANT a plugin requests
* (`{ services, hooks, network, fs }`; its legacy flat `string[]` form is
* retired). When a stack is
* assembled, the flatten order puts this collection on top — the stage table
* at {@link AssembledPackageBodySchema} states that precedence — so a
* manifest-stage `permissions` has no expression in an assembled body, and
* writing permission sets on the manifest instead of here reaches no reader.
*
* Both readings land in the SAME registry slot: `SchemaRegistry.installPackage`
* records `manifest.permissions` whichever stage produced it, so a consumer
* reading it off an installed-package row must name the stage it wants and
* report the other rather than dropping it silently.
*/
permissions: z.array(PermissionSetSchema).optional().describe('Permission Sets — the ADR-0090 collection half of `permissions`; at the manifest/authoring stage the same key is the ADR-0025 capability grant instead (`ManifestSchema.permissions`)'),
/**
* [ADR-0066 D1] Authorization capabilities this package DEFINES.
*
* The formal, EXPLICIT declaration entry point (`defineCapability`) — the
* package-side counterpart of the curated platform capabilities. Each entry
* is seeded into `sys_capability` at boot with `managed_by:'package'` +
* `package_id` provenance, instead of being implicitly derived (untitled)
* from whatever a permission set references in `systemPermissions[]`.
*
* NOT to be confused with `requires` (platform SERVICE capabilities like
* `ai`/`automation`) nor the runtime `ObjectStackCapabilities` descriptor —
* these are authorization capabilities in the ADR-0066 sense (referenced by
* `requiredPermissions` / granted by `systemPermissions`).
*/
capabilities: z.array(CapabilityDeclarationSchema).optional()
.describe('[ADR-0066 D1] Authorization capabilities this package defines (seeded with package provenance)'),
sharingRules: z.array(SharingRuleSchema).optional().describe('Record Sharing Rules'),
/**
* [ADR-0142 D1] Ownership queues this package DECLARES — named sets of
* people records can sit in until one of them claims the record. A declared
* queue's members name positions only; an organization staffs its own queues
* as `sys_queue` rows in Setup. Served from the registry, never copied into
* rows, and switched off through the activation ledger.
*
* A composition that declares queues must register the ownership-queue
* service (`@objectstack/plugin-sharing`): without it nothing resolves the
* names, so the boot is refused rather than served with inert queues.
*/
queues: z.array(QueueSchema).optional()
.describe('[ADR-0142 D1] Ownership queues — named sets of people records can sit in until a member claims them'),
/**
* [ADR-0142 D5] Assignment rules this package DECLARES: per object, ordered