@ultimat3/manifest 20.2.1 → 22.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/package.json +7 -6
- package/src/agents-md.ts +19 -1
- package/src/build.ts +13 -0
- package/src/diff-channels.ts +74 -0
- package/src/diff-entities.ts +9 -2
- package/src/diff-operations.ts +11 -5
- package/src/diff.ts +3 -0
- package/src/emit.ts +1 -0
- package/src/index.ts +1 -2
- package/src/schema.ts +31 -0
- package/src/sources.ts +13 -0
package/README.md
CHANGED
|
@@ -150,3 +150,15 @@ never writes, and it never synthesises a sentence.
|
|
|
150
150
|
| `X_MANIFEST_BREAKING` | contract broke with no major bump | bump the version, or restore |
|
|
151
151
|
| `X_AGENTS_MD_MISSING` | no `AGENTS.md` | write one by hand |
|
|
152
152
|
| `X_AGENTS_MD_TOO_LARGE` | over the byte budget | move facts to `x.manifest.json` |
|
|
153
|
+
|
|
154
|
+
### Error classes
|
|
155
|
+
|
|
156
|
+
Every error class `src/index.ts` exports, for `instanceof` inside one process. Across a wire or
|
|
157
|
+
a job boundary the class is gone and the `code` is what survives — match on that.
|
|
158
|
+
|
|
159
|
+
| Class | Code | Declared in |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `AgentsMdMissingError` | `X_AGENTS_MD_MISSING` | `src/errors.ts` |
|
|
162
|
+
| `AgentsMdTooLargeError` | `X_AGENTS_MD_TOO_LARGE` | `src/errors.ts` |
|
|
163
|
+
| `ManifestBreakingError` | `X_MANIFEST_BREAKING` | `src/errors.ts` |
|
|
164
|
+
| `ManifestDriftError` | `X_MANIFEST_DRIFT` | `src/errors.ts` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/manifest",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "22.0.0",
|
|
4
4
|
"description": "x.manifest.json: deterministic generated facts, contract diff, AGENTS.md budget",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,10 +31,11 @@
|
|
|
31
31
|
"test": "bun test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ultimat3/action": "
|
|
35
|
-
"@ultimat3/core": "
|
|
36
|
-
"@ultimat3/entity": "
|
|
37
|
-
"@ultimat3/jobs": "
|
|
38
|
-
"@ultimat3/query": "
|
|
34
|
+
"@ultimat3/action": "22.0.0",
|
|
35
|
+
"@ultimat3/core": "22.0.0",
|
|
36
|
+
"@ultimat3/entity": "22.0.0",
|
|
37
|
+
"@ultimat3/jobs": "22.0.0",
|
|
38
|
+
"@ultimat3/query": "22.0.0",
|
|
39
|
+
"@ultimat3/realtime": "22.0.0"
|
|
39
40
|
}
|
|
40
41
|
}
|
package/src/agents-md.ts
CHANGED
|
@@ -81,13 +81,31 @@ export async function assertAgentsMd(input: CheckAgentsMdInput = {}): Promise<Ag
|
|
|
81
81
|
return check;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/** A header naming a column of generated facts. A conventions table (`| Rule | Detail |`) names none. */
|
|
85
|
+
const FACT_COLUMN = /\b(column|entity|entities|route|routes|endpoint|endpoints|table|type)\b/i;
|
|
86
|
+
|
|
87
|
+
/** A table separator row: `| --- | :-: |`, pipes, dashes, colons and spaces only. */
|
|
88
|
+
const SEPARATOR = /^\s*\|[\s|:-]+\|\s*$/;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The HEADER row of every markdown table — the line directly above a separator. The columns are
|
|
92
|
+
* what a table tabulates; a word in a cell is prose about a convention, which is what the file is
|
|
93
|
+
* for, and warning on it flagged the scaffold's own conventions table on every app (slice 11 i).
|
|
94
|
+
*/
|
|
95
|
+
const tableHeaders = (text: string): readonly string[] => {
|
|
96
|
+
const lines = text.split('\n');
|
|
97
|
+
return lines.filter(
|
|
98
|
+
(line, index) => line.trim().startsWith('|') && SEPARATOR.test(lines[index + 1] ?? ''),
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
84
102
|
/**
|
|
85
103
|
* Warn when the file has started duplicating generated facts. Duplication is how a context
|
|
86
104
|
* file goes stale: the copy and the source drift, and the agent believes the copy.
|
|
87
105
|
*/
|
|
88
106
|
function warningsFor(text: string): readonly string[] {
|
|
89
107
|
const warnings: string[] = [];
|
|
90
|
-
if (
|
|
108
|
+
if (tableHeaders(text).some((header) => FACT_COLUMN.test(header))) {
|
|
91
109
|
warnings.push('looks like it tabulates schema or route facts; those live in x.manifest.json');
|
|
92
110
|
}
|
|
93
111
|
if (text.split('\n').length > 200) {
|
package/src/build.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { canonicalJson } from '@ultimat3/core';
|
|
20
20
|
import type {
|
|
21
21
|
ActionFact,
|
|
22
|
+
ChannelFact,
|
|
22
23
|
EntityFact,
|
|
23
24
|
ErrorCodeFact,
|
|
24
25
|
JobFact,
|
|
@@ -36,6 +37,7 @@ export interface ManifestSources {
|
|
|
36
37
|
readonly entities?: readonly EntityFact[];
|
|
37
38
|
readonly actions?: readonly ActionFact[];
|
|
38
39
|
readonly queries?: readonly QueryFact[];
|
|
40
|
+
readonly channels?: readonly ChannelFact[];
|
|
39
41
|
readonly jobs?: readonly JobFact[];
|
|
40
42
|
readonly tasks?: readonly TaskFact[];
|
|
41
43
|
readonly policies?: readonly PolicyFact[];
|
|
@@ -48,6 +50,7 @@ export function buildManifest(sources: ManifestSources): Manifest {
|
|
|
48
50
|
const entities = sortBy(sources.entities ?? [], (e) => e.name).map(normalizeEntity);
|
|
49
51
|
const actions = sortBy(sources.actions ?? [], (a) => a.name).map(normalizeAction);
|
|
50
52
|
const queries = sortBy(sources.queries ?? [], (q) => q.name).map(normalizeQuery);
|
|
53
|
+
const channels = sortBy(sources.channels ?? [], (c) => c.name).map(normalizeChannel);
|
|
51
54
|
const jobs = sortBy(sources.jobs ?? [], (j) => j.name).map(normalizeJob);
|
|
52
55
|
const tasks = sortBy(sources.tasks ?? [], (t) => t.name).map((t) => ({
|
|
53
56
|
...t,
|
|
@@ -70,6 +73,7 @@ export function buildManifest(sources: ManifestSources): Manifest {
|
|
|
70
73
|
...policies.map((p) => p.permission),
|
|
71
74
|
...actions.flatMap((a) => a.permissions),
|
|
72
75
|
...queries.flatMap((q) => q.permissions),
|
|
76
|
+
...channels.flatMap((c) => c.permissions),
|
|
73
77
|
]);
|
|
74
78
|
|
|
75
79
|
const body = {
|
|
@@ -79,6 +83,7 @@ export function buildManifest(sources: ManifestSources): Manifest {
|
|
|
79
83
|
entities,
|
|
80
84
|
actions,
|
|
81
85
|
queries,
|
|
86
|
+
channels,
|
|
82
87
|
jobs,
|
|
83
88
|
tasks,
|
|
84
89
|
policies,
|
|
@@ -152,6 +157,14 @@ const normalizeQuery = (query: QueryFact): QueryFact => ({
|
|
|
152
157
|
...(query.subscribes === undefined ? {} : { subscribes: [...query.subscribes].sort() }),
|
|
153
158
|
});
|
|
154
159
|
|
|
160
|
+
// `params` keep their DECLARED order — it is the order a topic's segments are spelled in, so it is
|
|
161
|
+
// part of the wire contract, not a set.
|
|
162
|
+
const normalizeChannel = (channel: ChannelFact): ChannelFact => ({
|
|
163
|
+
...channel,
|
|
164
|
+
records: [...channel.records].sort(),
|
|
165
|
+
permissions: [...channel.permissions].sort(),
|
|
166
|
+
});
|
|
167
|
+
|
|
155
168
|
// Job steps keep their DECLARED order — a job's steps are a sequence, not a set, and
|
|
156
169
|
// sorting them would misrepresent the program.
|
|
157
170
|
const normalizeJob = (job: JobFact): JobFact => ({ ...job, steps: [...job.steps] });
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Realtime channels in a contract diff. A client derives a channel's topic from its NAME and
|
|
2
|
+
// PARAMS, so either moving breaks every page built against the old ones; its subscribe policy is
|
|
3
|
+
// judged like any operation's, on the permissions it requires.
|
|
4
|
+
|
|
5
|
+
import { canonicalJson } from '@ultimat3/core';
|
|
6
|
+
import type { ManifestChange } from './diff-change';
|
|
7
|
+
import { index } from './diff-change';
|
|
8
|
+
import { diffPermissions } from './diff-operations';
|
|
9
|
+
import type { ChannelFact } from './schema';
|
|
10
|
+
|
|
11
|
+
export function diffChannels(
|
|
12
|
+
before: readonly ChannelFact[],
|
|
13
|
+
after: readonly ChannelFact[],
|
|
14
|
+
): readonly ManifestChange[] {
|
|
15
|
+
const changes: ManifestChange[] = [];
|
|
16
|
+
const afterByName = index(after, (c) => c.name);
|
|
17
|
+
const beforeByName = index(before, (c) => c.name);
|
|
18
|
+
|
|
19
|
+
for (const channel of before) {
|
|
20
|
+
const next = afterByName.get(channel.name);
|
|
21
|
+
const path = `channels.${channel.name}`;
|
|
22
|
+
if (next === undefined) {
|
|
23
|
+
changes.push({ kind: 'breaking', path, detail: 'channel removed' });
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (canonicalJson(channel.params) !== canonicalJson(next.params)) {
|
|
27
|
+
changes.push({ kind: 'breaking', path: `${path}.params`, detail: 'params changed' });
|
|
28
|
+
}
|
|
29
|
+
if (channel.policy !== next.policy) {
|
|
30
|
+
changes.push({
|
|
31
|
+
kind: 'breaking',
|
|
32
|
+
path: `${path}.policy`,
|
|
33
|
+
detail: `policy ${channel.policy} -> ${next.policy}`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
changes.push(...diffPermissions(path, channel, next));
|
|
37
|
+
// Losing a record type or the events stream breaks a page that renders them; gaining one
|
|
38
|
+
// breaks nobody.
|
|
39
|
+
for (const type of channel.records) {
|
|
40
|
+
if (!next.records.includes(type)) {
|
|
41
|
+
changes.push({
|
|
42
|
+
kind: 'breaking',
|
|
43
|
+
path: `${path}.records.${type}`,
|
|
44
|
+
detail: 'no longer carried',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const type of next.records) {
|
|
49
|
+
if (!channel.records.includes(type)) {
|
|
50
|
+
changes.push({ kind: 'additive', path: `${path}.records.${type}`, detail: 'now carried' });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (channel.events !== next.events) {
|
|
54
|
+
changes.push({
|
|
55
|
+
kind: next.events ? 'additive' : 'breaking',
|
|
56
|
+
path: `${path}.events`,
|
|
57
|
+
detail: `events ${String(channel.events)} -> ${String(next.events)}`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (channel.catchUp !== next.catchUp) {
|
|
61
|
+
changes.push({
|
|
62
|
+
kind: 'internal',
|
|
63
|
+
path: `${path}.catchUp`,
|
|
64
|
+
detail: `catch-up read ${channel.catchUp} -> ${next.catchUp}`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const channel of after) {
|
|
69
|
+
if (!beforeByName.has(channel.name)) {
|
|
70
|
+
changes.push({ kind: 'additive', path: `channels.${channel.name}`, detail: 'channel added' });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return changes;
|
|
74
|
+
}
|
package/src/diff-entities.ts
CHANGED
|
@@ -115,10 +115,17 @@ function diffColumns(
|
|
|
115
115
|
const beforeColumns = index(before.columns, (c) => c.name);
|
|
116
116
|
for (const column of after.columns) {
|
|
117
117
|
if (!beforeColumns.has(column.name)) {
|
|
118
|
+
// A default is what makes a NOT NULL column safe to add: every existing row takes it and no
|
|
119
|
+
// writer that omits the column is refused.
|
|
120
|
+
const additive = column.nullable || column.hasDefault === true;
|
|
118
121
|
changes.push({
|
|
119
|
-
kind:
|
|
122
|
+
kind: additive ? 'additive' : 'breaking',
|
|
120
123
|
path: `${path}.columns.${column.name}`,
|
|
121
|
-
detail: column.nullable
|
|
124
|
+
detail: column.nullable
|
|
125
|
+
? 'column added'
|
|
126
|
+
: additive
|
|
127
|
+
? 'column added with a default'
|
|
128
|
+
: 'NOT NULL column added with no default',
|
|
122
129
|
});
|
|
123
130
|
}
|
|
124
131
|
}
|
package/src/diff-operations.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// The two callable surfaces — actions and queries — and the permissions they require.
|
|
1
|
+
// The two callable surfaces — actions and queries — and the permissions they require. The
|
|
2
|
+
// permission rule is exported: a channel's subscribe requires permissions the same way.
|
|
2
3
|
|
|
3
4
|
import { canonicalJson, isMcpExposed } from '@ultimat3/core';
|
|
4
5
|
import type { ManifestChange } from './diff-change';
|
|
@@ -6,6 +7,11 @@ import { diffScalar, index } from './diff-change';
|
|
|
6
7
|
import { diffRateLimit } from './diff-rate-limit';
|
|
7
8
|
import type { ActionFact, QueryFact } from './schema';
|
|
8
9
|
|
|
10
|
+
/** Any fact that names the permissions it requires — an action, a query, a channel. */
|
|
11
|
+
export interface PermissionBearing {
|
|
12
|
+
readonly permissions: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
export function diffActions(
|
|
10
16
|
before: readonly ActionFact[],
|
|
11
17
|
after: readonly ActionFact[],
|
|
@@ -165,10 +171,10 @@ export function diffQueries(
|
|
|
165
171
|
* (`and(post:publish, org:administer)`) equals no permission, so a rule reading it would call
|
|
166
172
|
* every non-trivially-guarded operation unchanged while both of its real grants moved.
|
|
167
173
|
*/
|
|
168
|
-
function diffPermissions(
|
|
174
|
+
export function diffPermissions(
|
|
169
175
|
path: string,
|
|
170
|
-
before:
|
|
171
|
-
after:
|
|
176
|
+
before: PermissionBearing,
|
|
177
|
+
after: PermissionBearing,
|
|
172
178
|
): readonly ManifestChange[] {
|
|
173
179
|
const declared = readPermissions(before);
|
|
174
180
|
const next = readPermissions(after);
|
|
@@ -204,7 +210,7 @@ function diffPermissions(
|
|
|
204
210
|
}
|
|
205
211
|
|
|
206
212
|
/** The list as the FILE carries it, or `undefined` when it carries nothing this can compare. */
|
|
207
|
-
function readPermissions(fact:
|
|
213
|
+
function readPermissions(fact: PermissionBearing): readonly string[] | undefined {
|
|
208
214
|
const value: unknown = fact.permissions;
|
|
209
215
|
if (!Array.isArray(value)) return undefined;
|
|
210
216
|
return value.every((entry) => typeof entry === 'string')
|
package/src/diff.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import type { ManifestChange } from './diff-change';
|
|
15
15
|
import { diffNamedSet } from './diff-change';
|
|
16
|
+
import { diffChannels } from './diff-channels';
|
|
16
17
|
import { diffEntities } from './diff-entities';
|
|
17
18
|
import { diffActions, diffQueries } from './diff-operations';
|
|
18
19
|
import { diffErrorCodes, diffPolicies } from './diff-registries';
|
|
@@ -46,6 +47,8 @@ export function diffManifest(before: Manifest, after: Manifest): ManifestDiff {
|
|
|
46
47
|
|
|
47
48
|
changes.push(...diffActions(before.actions, after.actions));
|
|
48
49
|
changes.push(...diffQueries(before.queries, after.queries));
|
|
50
|
+
// `?? []`: a file written before channels were projected carries none, which is not a removal.
|
|
51
|
+
changes.push(...diffChannels(before.channels ?? [], after.channels ?? []));
|
|
49
52
|
changes.push(...diffRoutes(before.routes, after.routes));
|
|
50
53
|
changes.push(...diffJobs(before.jobs, after.jobs));
|
|
51
54
|
changes.push(...diffTasks(before.tasks, after.tasks));
|
package/src/emit.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -16,8 +16,6 @@ export { diffManifest, formatDiff } from './diff';
|
|
|
16
16
|
export type { DocEntry, DocEntryKind } from './docs-scan';
|
|
17
17
|
export {
|
|
18
18
|
headerComment,
|
|
19
|
-
parseGuideSections,
|
|
20
|
-
parseReExports,
|
|
21
19
|
scanInstalledDocs,
|
|
22
20
|
scanPackageDocs,
|
|
23
21
|
shortName,
|
|
@@ -44,6 +42,7 @@ export {
|
|
|
44
42
|
} from './errors';
|
|
45
43
|
export type {
|
|
46
44
|
ActionFact,
|
|
45
|
+
ChannelFact,
|
|
47
46
|
ColumnFact,
|
|
48
47
|
EntityFact,
|
|
49
48
|
ErrorCodeFact,
|
package/src/schema.ts
CHANGED
|
@@ -47,6 +47,12 @@ export interface ColumnFact {
|
|
|
47
47
|
readonly nullable: boolean;
|
|
48
48
|
readonly primaryKey?: boolean;
|
|
49
49
|
readonly references?: string;
|
|
50
|
+
/**
|
|
51
|
+
* The column has a declared default (a value, `defaultNow()`, a generated key). Written only when
|
|
52
|
+
* true, so a column without one reads as it always did. Adding a NOT NULL column WITH a default
|
|
53
|
+
* is additive — every existing row takes the default — and was classed breaking without this.
|
|
54
|
+
*/
|
|
55
|
+
readonly hasDefault?: boolean;
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
export interface EntityFact {
|
|
@@ -110,6 +116,24 @@ export interface QueryFact {
|
|
|
110
116
|
readonly cacheTags: readonly string[];
|
|
111
117
|
}
|
|
112
118
|
|
|
119
|
+
/**
|
|
120
|
+
* A declared realtime `channel()`: what a client subscribes by, and what its policy requires.
|
|
121
|
+
* Its NAME and PARAMS are the wire contract — a client spells neither, it derives both from the
|
|
122
|
+
* declaration — so a change to either is breaking for every page built against the old ones.
|
|
123
|
+
*/
|
|
124
|
+
export interface ChannelFact {
|
|
125
|
+
readonly name: string;
|
|
126
|
+
readonly params: readonly string[];
|
|
127
|
+
/** The query a client re-reads on `replay-gap`. */
|
|
128
|
+
readonly catchUp: string;
|
|
129
|
+
/** Record types (entity names) the channel carries. */
|
|
130
|
+
readonly records: readonly string[];
|
|
131
|
+
readonly events: boolean;
|
|
132
|
+
/** The policy's label. Never null since 22.0.0 — `channel()` requires a policy. */
|
|
133
|
+
readonly policy: string;
|
|
134
|
+
readonly permissions: readonly string[];
|
|
135
|
+
}
|
|
136
|
+
|
|
113
137
|
export interface JobFact {
|
|
114
138
|
readonly name: string;
|
|
115
139
|
readonly input: JsonValue;
|
|
@@ -156,6 +180,11 @@ export interface Manifest {
|
|
|
156
180
|
readonly entities: readonly EntityFact[];
|
|
157
181
|
readonly actions: readonly ActionFact[];
|
|
158
182
|
readonly queries: readonly QueryFact[];
|
|
183
|
+
/**
|
|
184
|
+
* Optional to a READER only: a file written before channels were projected has none, and that
|
|
185
|
+
* is "no channels", never an unreadable manifest. `buildManifest` always writes it.
|
|
186
|
+
*/
|
|
187
|
+
readonly channels?: readonly ChannelFact[];
|
|
159
188
|
readonly jobs: readonly JobFact[];
|
|
160
189
|
readonly tasks: readonly TaskFact[];
|
|
161
190
|
readonly policies: readonly PolicyFact[];
|
|
@@ -212,6 +241,8 @@ export function isManifest(value: unknown): value is Manifest {
|
|
|
212
241
|
const m = value as Record<string, unknown>;
|
|
213
242
|
if (typeof m['manifestVersion'] !== 'number' || typeof m['buildId'] !== 'string') return false;
|
|
214
243
|
for (const section of ARRAY_SECTIONS) if (!Array.isArray(m[section])) return false;
|
|
244
|
+
// Absent is a pre-channel file and readable; present and not an array is a damaged one.
|
|
245
|
+
if (m['channels'] !== undefined && !Array.isArray(m['channels'])) return false;
|
|
215
246
|
return isAppIdentity(m['app']);
|
|
216
247
|
}
|
|
217
248
|
|
package/src/sources.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { describeActions } from '@ultimat3/action';
|
|
|
9
9
|
import { describeEntities } from '@ultimat3/entity';
|
|
10
10
|
import { describeJobs } from '@ultimat3/jobs';
|
|
11
11
|
import { describeQueries } from '@ultimat3/query';
|
|
12
|
+
import { describeChannels } from '@ultimat3/realtime/server';
|
|
12
13
|
import type { ManifestSources } from './build';
|
|
13
14
|
import type { ErrorCodeFact, JsonValue, PolicyFact, RouteFact, TaskFact } from './schema';
|
|
14
15
|
|
|
@@ -53,6 +54,7 @@ export function frameworkSources(input: FrameworkSourcesInput): ManifestSources
|
|
|
53
54
|
type: column.kind,
|
|
54
55
|
nullable: !column.notNull,
|
|
55
56
|
primaryKey: column.primaryKey,
|
|
57
|
+
...(column.hasDefault ? { hasDefault: true } : {}),
|
|
56
58
|
...(column.references === null ? {} : { references: column.references }),
|
|
57
59
|
})),
|
|
58
60
|
invariants: entity.invariants.map((invariant) => invariant.name),
|
|
@@ -86,6 +88,17 @@ export function frameworkSources(input: FrameworkSourcesInput): ManifestSources
|
|
|
86
88
|
...(query.subscribes === null ? {} : { subscribes: query.subscribes }),
|
|
87
89
|
cacheTags: query.tags,
|
|
88
90
|
})),
|
|
91
|
+
// Already the fact's shape, sorted and flattened by `describeChannels` — projected field by
|
|
92
|
+
// field all the same, for the reason above.
|
|
93
|
+
channels: describeChannels().map((channel) => ({
|
|
94
|
+
name: channel.name,
|
|
95
|
+
params: channel.params,
|
|
96
|
+
catchUp: channel.catchUp,
|
|
97
|
+
records: channel.records,
|
|
98
|
+
events: channel.events,
|
|
99
|
+
policy: channel.policy,
|
|
100
|
+
permissions: channel.permissions,
|
|
101
|
+
})),
|
|
89
102
|
jobs: describeJobs().map((job) => ({
|
|
90
103
|
name: job.name,
|
|
91
104
|
input: asJson(job.input),
|