@crouter/api 0.3.377

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +67 -0
  2. package/dist/api/__tests__/error-codes.test.d.ts +1 -0
  3. package/dist/api/__tests__/error-codes.test.js +78 -0
  4. package/dist/api/__tests__/integration/client.test.d.ts +1 -0
  5. package/dist/api/__tests__/integration/client.test.js +179 -0
  6. package/dist/api/client.d.ts +467 -0
  7. package/dist/api/client.js +1179 -0
  8. package/dist/api/command-manifest/index.d.ts +3 -0
  9. package/dist/api/command-manifest/index.js +3 -0
  10. package/dist/api/command-manifest/manifest.d.ts +51 -0
  11. package/dist/api/command-manifest/manifest.js +332 -0
  12. package/dist/api/command-manifest/result.d.ts +25 -0
  13. package/dist/api/command-manifest/result.js +97 -0
  14. package/dist/api/command-manifest/schema.d.ts +28 -0
  15. package/dist/api/command-manifest/schema.js +856 -0
  16. package/dist/api/dto/analytics.d.ts +184 -0
  17. package/dist/api/dto/analytics.js +3 -0
  18. package/dist/api/dto/attach.d.ts +22 -0
  19. package/dist/api/dto/attach.js +13 -0
  20. package/dist/api/dto/bash-jobs.d.ts +24 -0
  21. package/dist/api/dto/bash-jobs.js +9 -0
  22. package/dist/api/dto/bash.d.ts +17 -0
  23. package/dist/api/dto/bash.js +1 -0
  24. package/dist/api/dto/broker-ops.d.ts +187 -0
  25. package/dist/api/dto/broker-ops.js +6 -0
  26. package/dist/api/dto/broker-signals.d.ts +25 -0
  27. package/dist/api/dto/broker-signals.js +1 -0
  28. package/dist/api/dto/broker.d.ts +86 -0
  29. package/dist/api/dto/broker.js +20 -0
  30. package/dist/api/dto/canvas.d.ts +359 -0
  31. package/dist/api/dto/canvas.js +2 -0
  32. package/dist/api/dto/chat-inventory.d.ts +56 -0
  33. package/dist/api/dto/chat-inventory.js +11 -0
  34. package/dist/api/dto/common.d.ts +29 -0
  35. package/dist/api/dto/common.js +15 -0
  36. package/dist/api/dto/config.d.ts +36 -0
  37. package/dist/api/dto/config.js +3 -0
  38. package/dist/api/dto/crons.d.ts +150 -0
  39. package/dist/api/dto/crons.js +10 -0
  40. package/dist/api/dto/custom-objects.d.ts +66 -0
  41. package/dist/api/dto/custom-objects.js +1 -0
  42. package/dist/api/dto/delivery.d.ts +71 -0
  43. package/dist/api/dto/delivery.js +7 -0
  44. package/dist/api/dto/docs.d.ts +135 -0
  45. package/dist/api/dto/docs.js +8 -0
  46. package/dist/api/dto/files.d.ts +21 -0
  47. package/dist/api/dto/files.js +1 -0
  48. package/dist/api/dto/focus.d.ts +24 -0
  49. package/dist/api/dto/focus.js +10 -0
  50. package/dist/api/dto/grants.d.ts +14 -0
  51. package/dist/api/dto/grants.js +1 -0
  52. package/dist/api/dto/health.d.ts +106 -0
  53. package/dist/api/dto/health.js +2 -0
  54. package/dist/api/dto/human-requests.d.ts +113 -0
  55. package/dist/api/dto/human-requests.js +4 -0
  56. package/dist/api/dto/human.d.ts +28 -0
  57. package/dist/api/dto/human.js +4 -0
  58. package/dist/api/dto/inbox.d.ts +273 -0
  59. package/dist/api/dto/inbox.js +4 -0
  60. package/dist/api/dto/lifecycle.d.ts +88 -0
  61. package/dist/api/dto/lifecycle.js +3 -0
  62. package/dist/api/dto/mail.d.ts +44 -0
  63. package/dist/api/dto/mail.js +1 -0
  64. package/dist/api/dto/messages.d.ts +88 -0
  65. package/dist/api/dto/messages.js +2 -0
  66. package/dist/api/dto/model-config.d.ts +25 -0
  67. package/dist/api/dto/model-config.js +1 -0
  68. package/dist/api/dto/modelauth.d.ts +132 -0
  69. package/dist/api/dto/modelauth.js +4 -0
  70. package/dist/api/dto/node-events.d.ts +65 -0
  71. package/dist/api/dto/node-events.js +4 -0
  72. package/dist/api/dto/node-outcomes.d.ts +88 -0
  73. package/dist/api/dto/node-outcomes.js +2 -0
  74. package/dist/api/dto/node-records.d.ts +35 -0
  75. package/dist/api/dto/node-records.js +5 -0
  76. package/dist/api/dto/nodes.d.ts +368 -0
  77. package/dist/api/dto/nodes.js +3 -0
  78. package/dist/api/dto/objects.d.ts +172 -0
  79. package/dist/api/dto/objects.js +5 -0
  80. package/dist/api/dto/profiles.d.ts +117 -0
  81. package/dist/api/dto/profiles.js +4 -0
  82. package/dist/api/dto/recovery.d.ts +104 -0
  83. package/dist/api/dto/recovery.js +1 -0
  84. package/dist/api/dto/reports.d.ts +93 -0
  85. package/dist/api/dto/reports.js +2 -0
  86. package/dist/api/dto/review-comments.d.ts +146 -0
  87. package/dist/api/dto/review-comments.js +5 -0
  88. package/dist/api/dto/reviews.d.ts +113 -0
  89. package/dist/api/dto/reviews.js +5 -0
  90. package/dist/api/dto/run-events.d.ts +293 -0
  91. package/dist/api/dto/run-events.js +6 -0
  92. package/dist/api/dto/subscriptions.d.ts +14 -0
  93. package/dist/api/dto/subscriptions.js +2 -0
  94. package/dist/api/dto/worktree.d.ts +55 -0
  95. package/dist/api/dto/worktree.js +6 -0
  96. package/dist/api/error-codes.d.ts +254 -0
  97. package/dist/api/error-codes.js +54 -0
  98. package/dist/api/errors.d.ts +47 -0
  99. package/dist/api/errors.js +66 -0
  100. package/dist/api/index.d.ts +42 -0
  101. package/dist/api/index.js +41 -0
  102. package/dist/api/node-transport.d.ts +18 -0
  103. package/dist/api/node-transport.js +105 -0
  104. package/dist/api/plugin-manifest-schema.d.ts +233 -0
  105. package/dist/api/plugin-manifest-schema.js +23 -0
  106. package/dist/api/routes.d.ts +160 -0
  107. package/dist/api/routes.js +193 -0
  108. package/dist/shared/generated-context.d.ts +79 -0
  109. package/dist/shared/generated-context.js +232 -0
  110. package/dist/shared/predicates.d.ts +2 -0
  111. package/dist/shared/predicates.js +4 -0
  112. package/package.json +49 -0
@@ -0,0 +1,3 @@
1
+ export * from './schema.js';
2
+ export * from './manifest.js';
3
+ export * from './result.js';
@@ -0,0 +1,3 @@
1
+ export * from './schema.js';
2
+ export * from './manifest.js';
3
+ export * from './result.js';
@@ -0,0 +1,51 @@
1
+ import type { DeclLeaf, DeclNode, TransportKind, ManifestTimeouts, CommandManifestIssue } from './schema.js';
2
+ export interface ValidatedCoreMount {
3
+ /** The explicitly extensible core branch receiving this plugin node. */
4
+ parent: string[];
5
+ node: DeclNode<DeclLeaf>;
6
+ }
7
+ export interface ValidatedCommandManifest {
8
+ schemaVersion: 1;
9
+ /** The plugin's declared integer major (HTTP transport only); absent means
10
+ * undeclared, which a locally installed plugin treats as 1. */
11
+ version?: number;
12
+ baseUrl?: string;
13
+ timeouts?: ManifestTimeouts;
14
+ /** Top-level plugin branches or repository-fragment nodes, ready to mount. */
15
+ roots: DeclNode<DeclLeaf>[];
16
+ /** Plugin nodes mounted below an explicitly extensible core branch. */
17
+ coreMounts: ValidatedCoreMount[];
18
+ /** Core command path (space-joined, e.g. "cron add") → attributed addendum
19
+ * text appended beneath that core command's help. Append-only product
20
+ * guidance — a plugin can never alter core contract text. */
21
+ helpAddenda?: Readonly<Record<string, string>>;
22
+ }
23
+ export interface CommandManifestValidation {
24
+ /** The validated, materialized manifest (present only if all issues are fixed). */
25
+ manifest?: ValidatedCommandManifest;
26
+ /** All typed validation issues. Any issue means zero contributions. */
27
+ issues: CommandManifestIssue[];
28
+ }
29
+ /**
30
+ * Validates a HTTP-transport plugin manifest exactly per §4, performing strict atomic
31
+ * validation and order-independent forest materialization. Returns either a
32
+ * valid manifest with roots ready to mount, or a complete issues list with
33
+ * zero contributions.
34
+ */
35
+ export declare function validateCommandManifest(raw: unknown, options: {
36
+ transport: TransportKind;
37
+ reservedCoreNames: ReadonlySet<string>;
38
+ /** Every core command path (space-joined). Supplied at the strict gates
39
+ * (install, bundle parse, doctor/inspect reports) so a helpAddenda key
40
+ * naming no core command fails there — and by the help-render lookup
41
+ * (`collectHelpAddenda`), which reproduces the gate's verdict so render
42
+ * and ingress report can never disagree. Omitted on the tolerant compose
43
+ * path, where mounting ignores addenda entirely, and on that lookup's
44
+ * claimant pass, which only needs to know whether any addendum targets the
45
+ * rendered path at all. */
46
+ coreCommandPaths?: ReadonlySet<string>;
47
+ /** A repository fragment attaches roots below an existing extensible branch. */
48
+ extensionFragment?: boolean;
49
+ /** Core branch names that accept plugin children. */
50
+ extensibleCoreBranches?: ReadonlySet<string>;
51
+ }): CommandManifestValidation;
@@ -0,0 +1,332 @@
1
+ // Transport-parameterized command manifest validation and forest materialization.
2
+ //
3
+ // Validates command manifests atomically, materializes self-contained forests
4
+ // independent of nested-mount order when the parent is inline, validates REST
5
+ // mappings, and enforces the canonical coalesced-body-source rule (exactly one
6
+ // stdin + one non-stdin flag/positional sharing a body `as`).
7
+ //
8
+ // This is pure validation: no fetch, no registration lookup, no file I/O beyond
9
+ // the raw manifest bytes passed in.
10
+ import { validateCommandNode } from './schema.js';
11
+ import { isRecord } from '../../shared/predicates.js';
12
+ // Validation helpers
13
+ const KEBAB = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
14
+ const REST_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
15
+ function typeName(v) {
16
+ if (v === null)
17
+ return 'null';
18
+ if (Array.isArray(v))
19
+ return 'array';
20
+ return typeof v;
21
+ }
22
+ // Top-level manifest validation
23
+ /**
24
+ * Validates a HTTP-transport plugin manifest exactly per §4, performing strict atomic
25
+ * validation and order-independent forest materialization. Returns either a
26
+ * valid manifest with roots ready to mount, or a complete issues list with
27
+ * zero contributions.
28
+ */
29
+ export function validateCommandManifest(raw, options) {
30
+ const issues = [];
31
+ const issue = (code, message, received, expected, next, path) => {
32
+ issues.push({ code, message, received, expected, next, ...(path ? { path } : {}) });
33
+ };
34
+ // Top-level structure check
35
+ if (!isRecord(raw)) {
36
+ issue('command_manifest_invalid', 'manifest must be an object', typeName(raw), options.extensionFragment === true ? '{ schemaVersion, transport, mounts }' : options.transport === 'http' ? '{ schemaVersion, version?, baseUrl?, timeouts?, mounts, helpAddenda? }' : '{ schemaVersion, mounts, helpAddenda? }', 'Provide a valid JSON manifest object.');
37
+ return { issues };
38
+ }
39
+ // Check for unknown top-level keys
40
+ const topKeys = Object.keys(raw);
41
+ const allowedTopKeys = options.extensionFragment === true
42
+ ? new Set(['schemaVersion', 'transport', 'mounts'])
43
+ : options.transport === 'http'
44
+ ? new Set(['schemaVersion', 'version', 'baseUrl', 'timeouts', 'mounts', 'helpAddenda'])
45
+ : new Set(['schemaVersion', 'mounts', 'helpAddenda']);
46
+ const unknownKeys = topKeys.filter((k) => !allowedTopKeys.has(k));
47
+ if (unknownKeys.length > 0) {
48
+ issue('command_manifest_invalid', `unknown top-level keys`, unknownKeys.join(', '), options.extensionFragment === true ? 'only: schemaVersion, transport, mounts' : options.transport === 'http' ? 'only: schemaVersion, version, baseUrl, timeouts, mounts, helpAddenda' : 'only: schemaVersion, mounts, helpAddenda', 'Remove the unknown keys.');
49
+ return { issues };
50
+ }
51
+ // Validate schemaVersion
52
+ if (raw['schemaVersion'] !== 1) {
53
+ issue('command_schema_version', `schemaVersion must be exactly 1`, String(raw['schemaVersion']), '1', 'Update the manifest schema version to 1.', 'schemaVersion');
54
+ return { issues };
55
+ }
56
+ // Validate optional plugin major version (HTTP only; the key is unknown elsewhere)
57
+ let version;
58
+ if (options.transport === 'http' && options.extensionFragment !== true && raw['version'] !== undefined) {
59
+ const v = raw['version'];
60
+ if (typeof v !== 'number' || !Number.isInteger(v) || v < 1) {
61
+ issue('command_manifest_invalid', 'version must be an integer of at least 1', typeName(v) === 'number' ? String(v) : typeName(v), 'the plugin major version as an integer >= 1, or omitted', 'Set version to the plugin\'s integer major.', 'version');
62
+ return { issues };
63
+ }
64
+ version = v;
65
+ }
66
+ // Validate optional baseUrl
67
+ let baseUrl;
68
+ if (options.transport === 'http' && raw['baseUrl'] !== undefined) {
69
+ if (typeof raw['baseUrl'] !== 'string') {
70
+ issue('command_manifest_invalid', 'baseUrl must be a string', typeName(raw['baseUrl']), 'an absolute HTTP(S) URL or omitted', 'Provide a valid baseUrl or remove it.', 'baseUrl');
71
+ return { issues };
72
+ }
73
+ try {
74
+ const url = new URL(raw['baseUrl']);
75
+ if (!['http:', 'https:'].includes(url.protocol)) {
76
+ issue('command_manifest_invalid', 'baseUrl must be http: or https:', url.protocol, 'http: or https:', 'Use an HTTP(S) URL.', 'baseUrl');
77
+ return { issues };
78
+ }
79
+ baseUrl = raw['baseUrl'];
80
+ }
81
+ catch {
82
+ issue('command_manifest_invalid', 'baseUrl must be a valid absolute URL', raw['baseUrl'], 'a valid URL', 'Fix the baseUrl.', 'baseUrl');
83
+ return { issues };
84
+ }
85
+ }
86
+ // Validate optional timeouts
87
+ let timeouts;
88
+ if (options.transport === 'http' && raw['timeouts'] !== undefined) {
89
+ const t = validateTimeouts(raw['timeouts'], issue);
90
+ if (t === null)
91
+ return { issues };
92
+ timeouts = t;
93
+ }
94
+ // Validate optional helpAddenda (both transports)
95
+ let helpAddenda;
96
+ if (raw['helpAddenda'] !== undefined) {
97
+ const h = validateHelpAddenda(raw['helpAddenda'], options.coreCommandPaths, issue);
98
+ if (h === null)
99
+ return { issues };
100
+ if (Object.keys(h).length > 0)
101
+ helpAddenda = h;
102
+ }
103
+ // Validate mounts (required, non-empty)
104
+ const mounts = raw['mounts'];
105
+ if (!Array.isArray(mounts)) {
106
+ issue('command_manifest_invalid', 'mounts must be an array', typeName(mounts), 'a non-empty array of { parent, node } contributions', 'Provide at least one mount.', 'mounts');
107
+ return { issues };
108
+ }
109
+ if (mounts.length === 0) {
110
+ issue('command_manifest_invalid', 'mounts must be a non-empty array', 'empty array', 'at least one contribution', 'Add at least one mount.', 'mounts');
111
+ return { issues };
112
+ }
113
+ // Validate and collect all mounts
114
+ const validatedMounts = [];
115
+ for (let i = 0; i < mounts.length; i++) {
116
+ const m = validateMount(mounts[i], i, options.transport, issue, options.extensionFragment === true ? { topLevelRootEntry: 'forbidden', allowPassthrough: false, allowExtensible: false } : undefined);
117
+ if (m === null)
118
+ return { issues };
119
+ validatedMounts.push(m);
120
+ }
121
+ // Materialize plugin-local trees separately from mounts below an explicitly
122
+ // extensible core branch. Core mounts remain nodes, never fake plugin roots.
123
+ const coreMounts = validatedMounts.filter((mount) => mount.parent.length > 0 && options.extensibleCoreBranches?.has(mount.parent.join(' ')) === true);
124
+ for (const mount of coreMounts) {
125
+ const parent = mount.parent.join(' ');
126
+ if (options.coreCommandPaths?.has(`${parent} ${mount.node.name}`) === true) {
127
+ issue('command_collision', `contributed command "${mount.node.name}" conflicts with a child the extensible core branch already owns`, mount.node.name, `a name not already owned by the ${parent} core branch`, 'Rename the contributed command or remove this mount.', `${mount.parent.join('.')}.${mount.node.name}`);
128
+ return { issues };
129
+ }
130
+ }
131
+ const roots = materializeForest(validatedMounts.filter((mount) => !coreMounts.includes(mount)), options.reservedCoreNames, options.extensibleCoreBranches, issue);
132
+ if (roots === null)
133
+ return { issues };
134
+ // If we got here with no issues, return the validated manifest
135
+ if (issues.length === 0) {
136
+ return {
137
+ manifest: {
138
+ schemaVersion: 1,
139
+ ...(version !== undefined ? { version } : {}),
140
+ ...(baseUrl !== undefined ? { baseUrl } : {}),
141
+ ...(timeouts !== undefined ? { timeouts } : {}),
142
+ ...(helpAddenda !== undefined ? { helpAddenda } : {}),
143
+ roots,
144
+ coreMounts,
145
+ },
146
+ issues: [],
147
+ };
148
+ }
149
+ // Any issue found -> zero contributions
150
+ return { issues };
151
+ }
152
+ // Timeouts validation
153
+ function validateTimeouts(raw, issue) {
154
+ if (!isRecord(raw)) {
155
+ issue('command_manifest_invalid', 'timeouts must be an object', typeName(raw), '{ connectMs?, requestMs?, streamIdleMs? }', 'Fix timeouts.', 'timeouts');
156
+ return null;
157
+ }
158
+ const allowedKeys = new Set(['connectMs', 'requestMs', 'streamIdleMs']);
159
+ const unknownKeys = Object.keys(raw).filter((k) => !allowedKeys.has(k));
160
+ if (unknownKeys.length > 0) {
161
+ issue('command_manifest_invalid', 'unknown timeouts keys', unknownKeys.join(', '), 'only: connectMs, requestMs, streamIdleMs', 'Remove the unknown keys.', 'timeouts');
162
+ return null;
163
+ }
164
+ const out = {};
165
+ for (const k of ['connectMs', 'requestMs', 'streamIdleMs']) {
166
+ if (raw[k] !== undefined) {
167
+ if (typeof raw[k] !== 'number' || !Number.isInteger(raw[k]) || raw[k] <= 0) {
168
+ issue('command_manifest_invalid', `${k} must be a positive integer`, String(raw[k]), 'a positive integer', `Fix ${k}.`, `timeouts.${k}`);
169
+ return null;
170
+ }
171
+ out[k] = raw[k];
172
+ }
173
+ }
174
+ return out;
175
+ }
176
+ // Help addenda validation
177
+ /** Validate the optional top-level `helpAddenda` map: core command path →
178
+ * addendum text rendered as an attributed block beneath that core command's
179
+ * help. When the caller supplies the core command path set, a key naming no
180
+ * existing core path is rejected outright — a typo fails at the gate, never
181
+ * silently at render. */
182
+ function validateHelpAddenda(raw, coreCommandPaths, issue) {
183
+ if (!isRecord(raw)) {
184
+ issue('command_manifest_invalid', 'helpAddenda must be an object', typeName(raw), 'a map of core command path → addendum text', 'Fix helpAddenda.', 'helpAddenda');
185
+ return null;
186
+ }
187
+ const out = {};
188
+ for (const [key, value] of Object.entries(raw)) {
189
+ if (key.split(' ').some((t) => !KEBAB.test(t))) {
190
+ issue('command_help_addendum_invalid', 'helpAddenda key must be a space-separated core command path', key, 'kebab tokens separated by single spaces (e.g. "cron" or "cron add")', 'Fix the helpAddenda key.', `helpAddenda.${key}`);
191
+ return null;
192
+ }
193
+ if (typeof value !== 'string' || value.length === 0) {
194
+ issue('command_manifest_invalid', 'helpAddenda value must be a non-empty string', typeof value === 'string' ? 'empty string' : typeName(value), 'non-empty addendum text', 'Fix the helpAddenda value.', `helpAddenda.${key}`);
195
+ return null;
196
+ }
197
+ if (coreCommandPaths !== undefined && !coreCommandPaths.has(key)) {
198
+ issue('command_help_addendum_invalid', 'helpAddenda key names no crtr core command path', key, 'an existing core command path (e.g. "cron", "cron add")', 'Fix the key or remove the addendum.', `helpAddenda.${key}`);
199
+ return null;
200
+ }
201
+ out[key] = value;
202
+ }
203
+ return out;
204
+ }
205
+ function validateMount(raw, index, transport, issue, nodeOptions) {
206
+ const path = `mounts[${index}]`;
207
+ if (!isRecord(raw)) {
208
+ issue('command_manifest_invalid', 'mount must be an object', typeName(raw), '{ parent, node }', 'Fix the mount.', path);
209
+ return null;
210
+ }
211
+ const allowedKeys = new Set(['parent', 'node']);
212
+ const unknownKeys = Object.keys(raw).filter((k) => !allowedKeys.has(k));
213
+ if (unknownKeys.length > 0) {
214
+ issue('command_manifest_invalid', 'unknown mount keys', unknownKeys.join(', '), 'only: parent, node', 'Remove the unknown keys.', path);
215
+ return null;
216
+ }
217
+ // Validate parent
218
+ const parentRaw = raw['parent'];
219
+ if (!Array.isArray(parentRaw)) {
220
+ issue('command_manifest_invalid', 'mount parent must be an array', typeName(parentRaw), 'an array of kebab tokens, or [] for a top-level mount', 'Fix parent.', `${path}.parent`);
221
+ return null;
222
+ }
223
+ for (let i = 0; i < parentRaw.length; i++) {
224
+ const t = parentRaw[i];
225
+ if (typeof t !== 'string' || !KEBAB.test(t)) {
226
+ issue('command_manifest_invalid', `parent token must be a kebab token`, String(t), 'a kebab-case identifier', 'Fix the parent path.', `${path}.parent[${i}]`);
227
+ return null;
228
+ }
229
+ }
230
+ const parent = parentRaw;
231
+ // Validate node
232
+ const nodeRaw = raw['node'];
233
+ const topLevel = parent.length === 0;
234
+ const node = validateCommandNode(nodeRaw, [`${path}.node`], topLevel, transport, issue, nodeOptions);
235
+ if (node === null)
236
+ return null;
237
+ return { parent, node };
238
+ }
239
+ // Forest materialization (order-independent)
240
+ /**
241
+ * Materializes a flat list of mounts into a forest of top-level branches,
242
+ * resolving nested mounts whose parent exists in a top-level inline tree regardless of order. Nested mounts that provide another nested mount's parent must appear first. Validates self-containment and no duplicates.
243
+ */
244
+ function materializeForest(mounts, reservedCoreNames, extensibleCoreBranches, issue) {
245
+ // Separate top-level (parent: []) from nested (parent non-empty)
246
+ const topLevel = mounts.filter((m) => m.parent.length === 0);
247
+ const nested = mounts.filter((m) => m.parent.length > 0);
248
+ // Check for core-name collisions in top-level nodes
249
+ for (const m of topLevel) {
250
+ if (reservedCoreNames.has(m.node.name)) {
251
+ issue('command_collision', `node name conflicts with a crtr core command`, m.node.name, `a name not in core (not one of: ${[...reservedCoreNames].join(', ')})`, 'Rename the node or remove this mount.', `${m.node.name}`);
252
+ return null;
253
+ }
254
+ }
255
+ // Build the forest: all top-level nodes with inline children, then attach nested mounts.
256
+ const roots = topLevel.map((m) => m.node.kind === 'branch'
257
+ ? { ...m.node, children: [...m.node.children] }
258
+ : { ...m.node });
259
+ for (const mount of nested) {
260
+ if (reservedCoreNames.has(mount.parent[0])) {
261
+ issue('command_parent_invalid', 'parent path starts with a core branch that is not extensible', mount.parent[0], `an extensible core branch (one of: ${[...(extensibleCoreBranches ?? [])].join(', ')})`, `Mount below one of the extensible core branches: ${[...(extensibleCoreBranches ?? [])].join(', ')}.`, mount.parent.join('.'));
262
+ return null;
263
+ }
264
+ }
265
+ // Attach nested mounts
266
+ for (const mount of nested) {
267
+ const resolved = resolveAndAttachMount(roots, mount, issue);
268
+ if (!resolved)
269
+ return null;
270
+ }
271
+ // Collect all paths as we materialize (for verification)
272
+ const pathIndex = new Set();
273
+ const indexPaths = (node, path) => {
274
+ const fullPath = path.join('.');
275
+ if (pathIndex.has(fullPath)) {
276
+ issue('command_node_invalid', `duplicate node path`, fullPath, 'unique paths within the manifest', 'Remove the duplicate node.', fullPath);
277
+ return false;
278
+ }
279
+ pathIndex.add(fullPath);
280
+ if (node.kind === 'branch') {
281
+ return node.children.every((child) => indexPaths(child, [...path, child.name]));
282
+ }
283
+ return true;
284
+ };
285
+ for (const root of roots) {
286
+ if (!indexPaths(root, [root.name]))
287
+ return null;
288
+ }
289
+ return roots;
290
+ }
291
+ /**
292
+ * Resolves a nested mount's parent path within the built roots and attaches
293
+ * the node. Validates that the parent exists and is a branch.
294
+ */
295
+ function resolveAndAttachMount(roots, mount, issue) {
296
+ const [first, ...rest] = mount.parent;
297
+ const root = roots.find((r) => r.name === first);
298
+ if (!root) {
299
+ issue('command_parent_invalid', `parent path starts with unknown branch`, first, 'a branch this manifest contributes', 'Fix the parent path or add the parent branch.', mount.parent.join('.'));
300
+ return false;
301
+ }
302
+ if (root.kind === 'leaf') {
303
+ issue('command_parent_invalid', 'parent path resolves to a leaf', first, 'a branch', 'Change the parent to point to a branch.', mount.parent.join('.'));
304
+ return false;
305
+ }
306
+ let current = root;
307
+ for (const token of rest) {
308
+ const child = current.children.find((c) => c.name === token);
309
+ if (!child) {
310
+ issue('command_parent_invalid', `parent path contains unknown branch`, token, 'a branch this manifest contributes', 'Fix the parent path or add the intermediate branch.', mount.parent.join('.'));
311
+ return false;
312
+ }
313
+ if (child.kind === 'leaf') {
314
+ issue('command_parent_invalid', `parent path resolves to a leaf`, token, 'a branch', 'Change the parent to point to a branch.', mount.parent.join('.'));
315
+ return false;
316
+ }
317
+ current = child;
318
+ }
319
+ // A passthrough owns every token below its path, so nested mounts would make
320
+ // the materialized branch invalid even though its inline children are empty.
321
+ if (current.passthrough !== undefined) {
322
+ issue('command_parent_invalid', 'parent path resolves to a passthrough branch', mount.parent.join('.'), 'a branch without passthrough', 'Remove the nested mount or remove passthrough from its parent branch.', mount.parent.join('.'));
323
+ return false;
324
+ }
325
+ // Attach the node
326
+ if (current.children.some((c) => c.name === mount.node.name)) {
327
+ issue('command_node_invalid', `duplicate child name`, mount.node.name, 'unique child names within a branch', 'Rename the node or remove the duplicate.', `${mount.parent.join('.')}.${mount.node.name}`);
328
+ return false;
329
+ }
330
+ current.children.push(mount.node);
331
+ return true;
332
+ }
@@ -0,0 +1,25 @@
1
+ export interface DeclaredOutputShape {
2
+ type: string;
3
+ constraint: string;
4
+ /** Named members of an `object` value. */
5
+ children?: DeclaredOutputField[];
6
+ /** Element shape of an `array` value. */
7
+ items?: DeclaredOutputShape;
8
+ /** Allowed values of an `enum` value. */
9
+ values?: string[];
10
+ }
11
+ export interface DeclaredOutputField extends DeclaredOutputShape {
12
+ name: string;
13
+ required: boolean;
14
+ }
15
+ /** Validates a result object against declared output field descriptors.
16
+ * Returns null if valid; otherwise returns a { field, receivedType } error. */
17
+ export declare function validateDeclaredResult(output: DeclaredOutputField[], result: Record<string, unknown>): {
18
+ field: string;
19
+ receivedType?: string;
20
+ } | null;
21
+ /** Core leaf declarations may explicitly require a nullable field. */
22
+ export declare function validateCoreDeclaredResult(output: DeclaredOutputField[], result: Record<string, unknown>): {
23
+ field: string;
24
+ receivedType?: string;
25
+ } | null;
@@ -0,0 +1,97 @@
1
+ import { isRecord } from '../../shared/predicates.js';
2
+ /** Validates a result object against declared output field descriptors.
3
+ * Returns null if valid; otherwise returns a { field, receivedType } error. */
4
+ export function validateDeclaredResult(output, result) {
5
+ return validateFields(output, result, true);
6
+ }
7
+ /** Core leaf declarations may explicitly require a nullable field. */
8
+ export function validateCoreDeclaredResult(output, result) {
9
+ return validateFields(output, result, true);
10
+ }
11
+ function validateFields(output, result, honorDeclaredNull) {
12
+ for (const f of output) {
13
+ const value = result[f.name];
14
+ const present = Object.prototype.hasOwnProperty.call(result, f.name)
15
+ && value !== undefined
16
+ && (honorDeclaredNull || value !== null);
17
+ if (f.required && !present)
18
+ return { field: f.name };
19
+ const matches = honorDeclaredNull ? coreTypeMatches(f, value) : typeMatches(f.type, value);
20
+ if (present && !matches) {
21
+ return { field: f.name, receivedType: value === null ? 'null' : typeof value };
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ /** Loose type check over the declared type vocabulary. Only the recognized
27
+ * primitive/container categories are enforced; an unrecognized declared type
28
+ * falls back to presence-only. */
29
+ function typeMatches(type, v) {
30
+ const t = type.toLowerCase();
31
+ if (t === 'string' || t === 'path')
32
+ return typeof v === 'string';
33
+ if (t === 'int' || t === 'integer')
34
+ return typeof v === 'number' && Number.isInteger(v);
35
+ if (t === 'number' || t === 'float')
36
+ return typeof v === 'number';
37
+ if (t === 'bool' || t === 'boolean')
38
+ return typeof v === 'boolean';
39
+ if (t === 'array' || t.endsWith('[]'))
40
+ return Array.isArray(v);
41
+ if (t === 'object')
42
+ return isRecord(v);
43
+ return true;
44
+ }
45
+ function coreTypeMatches(shape, v) {
46
+ const alternatives = shape.type.split('|').map((part) => part.trim()).filter(Boolean);
47
+ if (alternatives.length === 1 && !knownType(alternatives[0]))
48
+ return true;
49
+ const allKnown = alternatives.every((part) => knownType(part) || /^[a-z][a-z0-9_-]*$/.test(part));
50
+ if (!allKnown)
51
+ return true;
52
+ return alternatives.some((part) => atomMatches(part, v, shape));
53
+ }
54
+ function knownType(type) {
55
+ const t = type.toLowerCase();
56
+ return t === 'null'
57
+ || t === 'string'
58
+ || t === 'path'
59
+ || t === 'markdown'
60
+ || t === 'int'
61
+ || t === 'integer'
62
+ || t === 'number'
63
+ || t === 'float'
64
+ || t === 'bool'
65
+ || t === 'boolean'
66
+ || t === 'array'
67
+ || t.endsWith('[]')
68
+ || t === 'object'
69
+ || t === 'enum'
70
+ || t === 'file';
71
+ }
72
+ function atomMatches(type, v, shape) {
73
+ const t = type.toLowerCase();
74
+ if (t === 'null')
75
+ return v === null;
76
+ // An enum with declared `values` holds one of them; one without stays
77
+ // presence-only, as before `values` existed.
78
+ if (t === 'enum')
79
+ return shape.values === undefined || (typeof v === 'string' && shape.values.includes(v));
80
+ // A file output is the provider's file object on the wire, or the runtime path
81
+ // the daemon put in its place.
82
+ if (t === 'file')
83
+ return isRecord(v) || typeof v === 'string';
84
+ if (t === 'string' || t === 'path' || t === 'markdown')
85
+ return typeof v === 'string';
86
+ if (t === 'int' || t === 'integer')
87
+ return typeof v === 'number' && Number.isInteger(v);
88
+ if (t === 'number' || t === 'float')
89
+ return typeof v === 'number';
90
+ if (t === 'bool' || t === 'boolean')
91
+ return typeof v === 'boolean';
92
+ if (t === 'array' || t.endsWith('[]'))
93
+ return Array.isArray(v);
94
+ if (t === 'object')
95
+ return isRecord(v);
96
+ return typeof v === 'string' && v === type;
97
+ }
@@ -0,0 +1,28 @@
1
+ import type { ManifestRootEntry as DeclRootEntry, ManifestPassthrough as DeclPassthrough, ManifestLeafBase as DeclLeafBase, ManifestExecLeaf as ExecDeclLeaf, ManifestHttpLeaf as HttpDeclLeaf, ManifestLeaf as DeclLeaf, ManifestBranch as DeclBranch, ManifestNode as DeclNode, RestMethod, RestParamPlacement, RestParamMapping, RestMapping, ManifestTimeouts } from '../plugin-manifest-schema.js';
2
+ export type { DeclRootEntry, DeclPassthrough, DeclLeafBase, ExecDeclLeaf, HttpDeclLeaf, DeclLeaf, DeclBranch, DeclNode, RestMethod, RestParamPlacement, RestParamMapping, RestMapping, ManifestTimeouts, };
3
+ export interface CommandManifestIssue {
4
+ code: CommandIssueCode;
5
+ path?: string;
6
+ message: string;
7
+ received: string;
8
+ expected: string;
9
+ next: string;
10
+ }
11
+ export type CommandIssueCode = 'command_manifest_unreadable' | 'command_manifest_invalid' | 'command_schema_version' | 'command_path_unsafe' | 'command_not_executable' | 'command_parent_invalid' | 'command_node_invalid' | 'command_collision' | 'command_help_addendum_invalid' | 'command_rest_invalid' | 'command_extension_invalid';
12
+ type IssueFn = (code: CommandIssueCode, message: string, received: string, expected: string, next: string, path?: string) => void;
13
+ /** Base type of a declared output type: the union with every `null` part
14
+ * dropped, lower-cased. It decides which structural keys the shape may carry. */
15
+ export declare function outputBaseType(type: string): string;
16
+ /** Whether a declared output type names `file` anywhere: as a union part or
17
+ * as the element of an array type. */
18
+ export declare function mentionsFile(type: string): boolean;
19
+ export type TransportKind = 'exec' | 'http';
20
+ export interface CommandNodeValidationOptions {
21
+ /** Plugin roots need rootEntry; repository-fragment roots attach under an existing branch and forbid it. */
22
+ topLevelRootEntry?: 'required' | 'forbidden';
23
+ /** Repository fragments have no passthrough escape hatch. */
24
+ allowPassthrough?: boolean;
25
+ /** Only plugin manifests may mark their own top-level branch extensible. */
26
+ allowExtensible?: boolean;
27
+ }
28
+ export declare function validateCommandNode(raw: unknown, path: string[], topLevel: boolean, transport: TransportKind, issue: IssueFn, options?: CommandNodeValidationOptions): DeclBranch<DeclLeaf> | DeclLeaf | null;