@unoverse-platform/studio 0.1.116 → 0.1.117
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/local/scaffold.d.mts +1 -0
- package/local/scaffold.mjs +12 -0
- package/package.json +1 -1
- package/screens/AgentChain.tsx +41 -25
- package/screens/AgentsView.tsx +41 -1
- package/screens/TemplatesLibraryView.tsx +32 -3
- package/screens/TemplatesView.tsx +2 -2
- package/screens/index.ts +1 -1
- package/src/host.ts +5 -0
- package/src/index.css +11 -0
- package/vendor/base/src/agent-mcp/discovery.ts +1 -1
- package/vendor/base/src/agent-mcp/index.ts +3 -1
- package/vendor/base/src/agent-mcp/invoke.ts +62 -18
- package/vendor/base/src/agent-mcp/progressive.ts +124 -0
- package/vendor/base/src/agent-mcp/tools.ts +1 -1
- package/vendor/base/src/definitions/load/expand/inherit.ts +1 -1
- package/vendor/base/src/definitions/load/expand/refs.ts +1 -1
- package/vendor/base/src/definitions/load/expand/walk.ts +1 -1
- package/vendor/base/src/definitions/registry/agentFolders.ts +2 -2
- package/vendor/base/src/definitions/registry/catalog.ts +1 -1
- package/vendor/base/src/definitions/registry/objects.ts +94 -6
- package/vendor/base/src/definitions/registry/pipelines.ts +13 -2
- package/vendor/base/src/http/uwsFetchAdapter.ts +39 -3
- package/vendor/base/src/items/collect.ts +10 -0
- package/vendor/base/src/items/publish.ts +10 -0
- package/vendor/base/src/lint/design/index.mjs +4 -1
- package/vendor/base/src/lint/design/vocabulary.mjs +1 -1
- package/vendor/base/src/lint/objects/index.mjs +1 -1
- package/vendor/base/src/lint/objects/rules.d.mts +3 -0
- package/vendor/base/src/lint/objects/rules.mjs +93 -5
- package/vendor/base/src/manifests/compose.ts +3 -0
- package/vendor/base/src/manifests/executor/callback.ts +3 -2
- package/vendor/base/src/manifests/executor/memory.ts +27 -0
- package/vendor/base/src/manifests/executor/toolBridge.ts +70 -11
- package/vendor/base/src/manifests/runtime/auth/oauth.ts +43 -10
- package/vendor/base/src/manifests/runtime/events.ts +1 -1
- package/vendor/base/src/manifests/runtime/http/request.ts +43 -3
- package/vendor/base/src/manifests/runtime/loops/poll.ts +27 -5
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +1 -1
- package/vendor/base/src/platform/inbound.ts +67 -0
- package/vendor/base/src/platform/tokenCost.ts +27 -8
- package/vendor/sdk/lib/core/actions.ts +10 -0
- package/vendor/sdk/lib/core/apps.ts +2 -2
- package/vendor/sdk/lib/core/client.ts +11 -0
- package/vendor/sdk/lib/core/store.ts +1 -1
- package/vendor/sdk/lib/core/types.ts +2 -0
- package/vendor/sdk/lib/dom/app.ts +3 -2
- package/vendor/sdk/lib/dom/component.ts +38 -1
- package/vendor/sdk/lib/dom/element.ts +23 -0
- package/vendor/sdk/lib/dom/primitives.ts +97 -6
- package/vendor/sdk/lib/voiceService.ts +3 -1
- package/vendor/sdk/main.ts +1 -1
|
@@ -83,7 +83,7 @@ function applyStyleLiterals(style: unknown, lits: Record<string, unknown>): void
|
|
|
83
83
|
|
|
84
84
|
export function applyLiterals(node: AnyNode, lits: Record<string, unknown>): void {
|
|
85
85
|
// A NESTED REF FORWARDS THE LITERAL. A piece whose kind is itself a Ref to an atom
|
|
86
|
-
// (
|
|
86
|
+
// (a bank's button: primary = the marketplace `button` atom with `props: {label: label}`)
|
|
87
87
|
// hands its host's literal on as that Ref's `with`, so the atom draws the words whether
|
|
88
88
|
// the tree is expanded here or resolved by name on the client (referenced form).
|
|
89
89
|
if (node.type === "Ref" && node.props && typeof node.props === "object") {
|
|
@@ -61,7 +61,7 @@ export function expandNode(node: AnyNode, contextOrg?: string, inherit?: Record<
|
|
|
61
61
|
* silently resolved to the marketplace `button` atom and the org's own button component
|
|
62
62
|
* was never reached (2026-09-07). A bare name keeps the old order: atom first.
|
|
63
63
|
*
|
|
64
|
-
* AN EMBEDDED PIECE'S PRIVATE AXIS IS CHOSEN WHERE IT IS COMPOSED (2026-09-07, the
|
|
64
|
+
* AN EMBEDDED PIECE'S PRIVATE AXIS IS CHOSEN WHERE IT IS COMPOSED (2026-09-07, the a bank
|
|
65
65
|
* button: kinds primary/secondary/outline/link on a private `kind` axis). Its root is the
|
|
66
66
|
* compiled surface shell around the switches on that axis. Inlined, the piece has no slice
|
|
67
67
|
* of its own, so the host's `with` names the kind, and a host that names none gets the
|
|
@@ -37,7 +37,7 @@ export interface AgentFolder {
|
|
|
37
37
|
version?: string;
|
|
38
38
|
avatar?: string;
|
|
39
39
|
skills: string[];
|
|
40
|
-
model?: { use?: string; reasoning?: string; fallback?: string };
|
|
40
|
+
model?: { use?: string; reasoning?: string; fallback?: string; turns?: number };
|
|
41
41
|
budget?: { perDay?: string; perRun?: string };
|
|
42
42
|
tasks: string[];
|
|
43
43
|
access: string[];
|
|
@@ -79,7 +79,7 @@ export function readAgentFolder(root: string, org: string, name: string): AgentF
|
|
|
79
79
|
version: str(doc.version),
|
|
80
80
|
avatar: str(doc.avatar),
|
|
81
81
|
skills: strs(doc.skills),
|
|
82
|
-
...(model ? { model: { use: str(model.use), reasoning: str(model.reasoning), fallback: str(model.fallback) } } : {}),
|
|
82
|
+
...(model ? { model: { use: str(model.use), reasoning: str(model.reasoning), fallback: str(model.fallback), ...(typeof model.turns === "number" && model.turns > 0 ? { turns: Math.floor(model.turns) } : {}) } } : {}),
|
|
83
83
|
...(budget ? { budget: { perDay: str(budget.perDay), perRun: str(budget.perRun) } } : {}),
|
|
84
84
|
tasks: strs(doc.tasks),
|
|
85
85
|
access: strs(doc.access),
|
|
@@ -290,7 +290,7 @@ export function loadAppManifest(ref: string): AppManifest | null {
|
|
|
290
290
|
// the template loader); the app loader never did, so `app.previewComponents?.[0]`
|
|
291
291
|
// was undefined and the app tool's whole render block — gated on that ref — was
|
|
292
292
|
// skipped. The call returned cleanly, nothing reached /stream, and the model told
|
|
293
|
-
// the person to use a surface that had never appeared (
|
|
293
|
+
// the person to use a surface that had never appeared (a bank's voice app, 2026-09-07).
|
|
294
294
|
// `kind` rides along for the same reason it does on a template: the render path
|
|
295
295
|
// resolves the definition in the tier `kind` names, and an app is not a component.
|
|
296
296
|
return normalizeMode({
|
|
@@ -94,6 +94,8 @@ export interface ObjectLink {
|
|
|
94
94
|
match?: Record<string, string>;
|
|
95
95
|
/** Fields (of `between`, or the row's side of `match`) whose values may name one thing several ways: one question per set of names says which are the same. */
|
|
96
96
|
alike?: string[];
|
|
97
|
+
/** Fields of the row this line carries, added never replaced: a provider's `serves` carries its role (relationships-and-labels.md). */
|
|
98
|
+
carries?: string[];
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
/**
|
|
@@ -112,8 +114,9 @@ export interface ObjectMatching {
|
|
|
112
114
|
evidence?: string[];
|
|
113
115
|
/** Filled on both and different, as words: never the same thing. */
|
|
114
116
|
ruleOut?: string[];
|
|
115
|
-
/** The node that decides a name the rules leave open (TypeSafeChoice)
|
|
116
|
-
|
|
117
|
+
/** The node that decides a name the rules leave open: a node's name (TypeSafeChoice), or a decision node with its
|
|
118
|
+
* config ({ node: OpenAIDecision, config: { model } }), asked as the review is. Absent, an open name goes to review. */
|
|
119
|
+
judge?: string | { node: string; config?: Record<string, unknown> };
|
|
117
120
|
/**
|
|
118
121
|
* THE LINKER, BEFORE THE JUDGE (pipelines README, "Three sweeps"; owner 2026-10-10: "Splink to dedupe
|
|
119
122
|
* our entities"): a node that scores the name against each candidate's names. Sure the same, it links;
|
|
@@ -129,6 +132,8 @@ export interface ObjectMatching {
|
|
|
129
132
|
review?: { node: string; config?: Record<string, unknown> };
|
|
130
133
|
/** The lines (by name, e.g. a document's `about`) whose name, sure to be no record held, makes the record. A mention on any other line never does. */
|
|
131
134
|
createFrom?: string[];
|
|
135
|
+
/** The object's `extract.never` words, set by the registry: left out when two names of one parent are compared ("A Shares" and "Class A"). */
|
|
136
|
+
never?: string[];
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
/**
|
|
@@ -143,6 +148,18 @@ export interface DatasetColumn {
|
|
|
143
148
|
* its words here and the database refuses the rest: a misspelling cannot become a second
|
|
144
149
|
* thing that never sums with the first. Absent on a column carrying a document's own words. */
|
|
145
150
|
enum?: string[];
|
|
151
|
+
/** FOR A COLUMN WITH `enum`: the words a table's title or headings print for each value (net return:
|
|
152
|
+
* [performance, returns]), so a table found by its shape is known by what it holds (tables.md 3b). */
|
|
153
|
+
wordings?: Record<string, string[]>;
|
|
154
|
+
/** FOR A COLUMN WITH `enum`: the words a key-figures row's own label prints for each value (net asset
|
|
155
|
+
* value: [total net assets, fund size]), so "Total Net Assets | USD 126m" is one figure (tables.md 3b). */
|
|
156
|
+
rowWordings?: Record<string, string[]>;
|
|
157
|
+
/** FOR A COLUMN WITH `enum`: the words a table's COLUMN prints for each value (exposure: [market value, % of net
|
|
158
|
+
* assets]), so a number kept from any table is known by the column it sits under (tables.md 3f, 2026-10-16). */
|
|
159
|
+
columnWordings?: Record<string, string[]>;
|
|
160
|
+
/** FOR A COLUMN WITH `enum`: the values of another column each of its values gathers (dimension fair value level:
|
|
161
|
+
* [Level 1, Level 2, Level 3]), so a number whose row or column names one is known to be sliced that way (tables.md 3i). */
|
|
162
|
+
values?: Record<string, string[]>;
|
|
146
163
|
/** string | number | integer | date | datetime | boolean. Nothing else: a type this does not
|
|
147
164
|
* name is one the generated table cannot promise anything about. */
|
|
148
165
|
type: string;
|
|
@@ -165,6 +182,35 @@ export interface DatasetDefinition {
|
|
|
165
182
|
subject?: string;
|
|
166
183
|
asOf?: string;
|
|
167
184
|
description?: string;
|
|
185
|
+
/** WHOSE A ROW IS, AS RECORDS (tables.md 3i step 4, 2026-10-17: the code names no industry): the object a row's subject is,
|
|
186
|
+
* its parts, how one sits inside another, and the `level` values each is filed under. Absent: rows name no record. */
|
|
187
|
+
whose?: DatasetWhose;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface DatasetWhose {
|
|
191
|
+
/** The object a row's subject is (a fund), the column its name goes in, the `level` value for its own figures. */
|
|
192
|
+
object: string; column: string; level: string;
|
|
193
|
+
/** The subject's field of other names, the field naming what it is compared with (its index), and the `level` value for that. */
|
|
194
|
+
names?: string; compare?: string; compareLevel?: string;
|
|
195
|
+
/** How a subject sits inside another: the link from it to its parent, and the field naming the parent. */
|
|
196
|
+
within?: { link: string; field?: string };
|
|
197
|
+
/** Its parts (a fund's share classes): their object, their column, their `level` value, their name field, and the field naming their subject. */
|
|
198
|
+
part?: { object: string; column: string; level: string; name: string; of: string; measures?: string[]; perUnit?: Record<string, string> };
|
|
199
|
+
/** The subject's own measures: a figure of one is the subject's, over a part only its section named. */
|
|
200
|
+
measures?: string[];
|
|
201
|
+
/** The measures that are a record's standing terms (a fee rate): a figure of one is a field, never a row. */
|
|
202
|
+
terms?: string[];
|
|
203
|
+
/** The measures that are a subject's figure against its comparison (an excess return): a row of one that names no
|
|
204
|
+
* subject is the subject the table's own rows name, when they name exactly one. */
|
|
205
|
+
relative?: string[];
|
|
206
|
+
/** The words a name of the subject's kind carries (fund, portfolio): an allocation's row naming one, with words of its
|
|
207
|
+
* own, is a holding (tables.md 3c rule 3). Absent, the word fund. */
|
|
208
|
+
vehicles?: string[];
|
|
209
|
+
/** Who stands behind the subject (a fund's manager): its object, as a layout's section names it in `about`, and the
|
|
210
|
+
* `level` value for its figures (the firm's assets under management), reached from a row's subject by a join. */
|
|
211
|
+
owner?: { object: string; level: string };
|
|
212
|
+
/** The `level` value for a document about no subject (a market note). */
|
|
213
|
+
none?: string;
|
|
168
214
|
}
|
|
169
215
|
|
|
170
216
|
export interface ObjectFunction {
|
|
@@ -220,7 +266,7 @@ export interface ObjectDefinition {
|
|
|
220
266
|
/** How a name is matched to its rows (entity-resolution.md); absent, a line to it compares one field with one field. */
|
|
221
267
|
matching?: ObjectMatching;
|
|
222
268
|
/** How a page reader picks it out of a page (pipelines README, "Or read by a node"): instructions and worked examples. */
|
|
223
|
-
extract?: { instructions?: string; examples?: { text: string; answer: Record<string, unknown>[] }[]; never?: string[] };
|
|
269
|
+
extract?: { instructions?: string; examples?: { text: string; answer: Record<string, unknown>[] }[]; never?: string[]; notAlso?: string[]; notAlsoWhen?: Record<string, Record<string, string[]>>; notWhen?: string[] };
|
|
224
270
|
/** THE TABLE THIS OBJECT OWNS (docs/unoverse/data/objects/dataset.md, PROPOSED 2026-09-18),
|
|
225
271
|
* from its folder's table.yaml: an object that holds many small facts read together rather
|
|
226
272
|
* than one row read alone. Absent on every ordinary object, which stays a row. */
|
|
@@ -290,12 +336,54 @@ export function readDataset(name: string, file: Record<string, any>, dir: string
|
|
|
290
336
|
// about the column.
|
|
291
337
|
if (allowed && type !== "string") findings.push(`column "${col}": only a string column may say which values it allows`);
|
|
292
338
|
if (allowed && new Set(allowed).size !== allowed.length) findings.push(`column "${col}": its allowed values repeat`);
|
|
293
|
-
|
|
339
|
+
// ITS WORDS (tables.md 3b): what a table prints for each allowed value; a key outside the list is no word of it.
|
|
340
|
+
const wordings = c?.wordings && typeof c.wordings === "object" ? Object.fromEntries(Object.entries(c.wordings as Record<string, unknown>).map(([k, v]) => [k, Array.isArray(v) ? v.map(String) : []])) : undefined;
|
|
341
|
+
if (wordings && !allowed) findings.push(`column "${col}": wordings name the words for allowed values, so the column says enum`);
|
|
342
|
+
for (const k of Object.keys(wordings ?? {})) if (allowed && !allowed.includes(k)) findings.push(`column "${col}": wordings for "${k}", which is not one of its allowed values`);
|
|
343
|
+
// AND A ROW'S OWN WORDS (key figures, tables.md 3b): kept like `wordings`, or the reader never sees them
|
|
344
|
+
// (measured 2026-10-14: dropped here, no key figure was read).
|
|
345
|
+
const rowWordings = c?.rowWordings && typeof c.rowWordings === "object" ? Object.fromEntries(Object.entries(c.rowWordings as Record<string, unknown>).map(([k, v]) => [k, Array.isArray(v) ? v.map(String) : []])) : undefined;
|
|
346
|
+
if (rowWordings && !allowed) findings.push(`column "${col}": rowWordings name the words for allowed values, so the column says enum`);
|
|
347
|
+
for (const k of Object.keys(rowWordings ?? {})) if (allowed && !allowed.includes(k)) findings.push(`column "${col}": rowWordings for "${k}", which is not one of its allowed values`);
|
|
348
|
+
// AND A COLUMN'S OWN WORDS (every number kept, tables.md 3f): kept like `wordings`.
|
|
349
|
+
const columnWordings = c?.columnWordings && typeof c.columnWordings === "object" ? Object.fromEntries(Object.entries(c.columnWordings as Record<string, unknown>).map(([k, v]) => [k, Array.isArray(v) ? v.map(String) : []])) : undefined;
|
|
350
|
+
if (columnWordings && !allowed) findings.push(`column "${col}": columnWordings name the words for allowed values, so the column says enum`);
|
|
351
|
+
for (const k of Object.keys(columnWordings ?? {})) if (allowed && !allowed.includes(k)) findings.push(`column "${col}": columnWordings for "${k}", which is not one of its allowed values`);
|
|
352
|
+
// AND THE VALUES EACH OF ITS VALUES GATHERS (a slicing's own values, tables.md 3i): kept like `wordings`.
|
|
353
|
+
const values = c?.values && typeof c.values === "object" ? Object.fromEntries(Object.entries(c.values as Record<string, unknown>).map(([k, v]) => [k, Array.isArray(v) ? v.map(String) : []])) : undefined;
|
|
354
|
+
if (values && !allowed) findings.push(`column "${col}": values name what allowed values gather, so the column says enum`);
|
|
355
|
+
for (const k of Object.keys(values ?? {})) if (allowed && !allowed.includes(k)) findings.push(`column "${col}": values for "${k}", which is not one of its allowed values`);
|
|
356
|
+
columns.push({ name: col, type, ...(allowed ? { enum: allowed } : {}), ...(wordings ? { wordings } : {}), ...(rowWordings ? { rowWordings } : {}), ...(columnWordings ? { columnWordings } : {}), ...(values ? { values } : {}), ...(c?.required === true ? { required: true } : {}), ...(c?.indexed === true ? { indexed: true } : {}), ...(c?.description ? { description: String(c.description) } : {}) });
|
|
294
357
|
}
|
|
295
358
|
const uniqueBy = Array.isArray(file.uniqueBy) ? file.uniqueBy.map((u: unknown) => String(u)) : undefined;
|
|
296
359
|
for (const u of uniqueBy ?? []) if (!seen.has(u) && !RESERVED.has(u)) findings.push(`uniqueBy names "${u}", which is not a column`);
|
|
297
360
|
if (findings.length) throw new Error(`object ${name}: ${findings.join("; ")} (${dir})`);
|
|
298
|
-
|
|
361
|
+
// WHOSE A ROW IS, AS RECORDS: kept as declared, its columns checked.
|
|
362
|
+
const w = file.whose && typeof file.whose === "object" ? (file.whose as Record<string, any>) : null;
|
|
363
|
+
const whose: DatasetWhose | undefined = w && w.object && w.column && w.level ? {
|
|
364
|
+
object: String(w.object), column: String(w.column), level: String(w.level),
|
|
365
|
+
...(w.names ? { names: String(w.names) } : {}), ...(w.compare ? { compare: String(w.compare) } : {}), ...(w.compareLevel ? { compareLevel: String(w.compareLevel) } : {}),
|
|
366
|
+
...(w.within?.link ? { within: { link: String(w.within.link), ...(w.within.field ? { field: String(w.within.field) } : {}) } } : {}),
|
|
367
|
+
// A PART'S OWN MEASURES (2026-10-24): a figure of one of them is always a part's ("net asset value per share" is a
|
|
368
|
+
// share class's; a fund has no price per share of its own), whatever section it is printed in.
|
|
369
|
+
...(w.part?.object ? { part: { object: String(w.part.object), column: String(w.part.column), level: String(w.part.level), name: String(w.part.name), of: String(w.part.of), ...(Array.isArray(w.part.measures) ? { measures: w.part.measures.map(String) } : {}), ...(w.part.perUnit && typeof w.part.perUnit === "object" ? { perUnit: Object.fromEntries(Object.entries(w.part.perUnit).map(([k, v]) => [k, String(v)])) } : {}) } } : {}),
|
|
370
|
+
...(w.none ? { none: String(w.none) } : {}),
|
|
371
|
+
...(Array.isArray(w.measures) ? { measures: w.measures.map(String) } : {}),
|
|
372
|
+
// THE MEASURES THAT ARE TERMS (2026-10-25): a figure of one is a field of its record, never a row ("static goes into
|
|
373
|
+
// the entities, changes over time goes into the metrics").
|
|
374
|
+
...(Array.isArray(w.terms) ? { terms: w.terms.map(String) } : {}),
|
|
375
|
+
// THE MEASURES THAT ARE A FIGURE AGAINST A COMPARISON (2026-10-25): "Excess" printed under a class's and its index's
|
|
376
|
+
// rows is that class's return above the index, as the measure's own definition says.
|
|
377
|
+
...(Array.isArray(w.relative) ? { relative: w.relative.map(String) } : {}),
|
|
378
|
+
// WHAT A HOLDING OF THE SUBJECT'S KIND IS CALLED IN ITS NAME (tables.md 3c rule 3, 2026-10-26): a fund, a segregated portfolio.
|
|
379
|
+
...(Array.isArray(w.vehicles) ? { vehicles: w.vehicles.map(String) } : {}),
|
|
380
|
+
// WHO STANDS BEHIND THE SUBJECT (2026-10-25): a section about a fund's manager prints the firm's figures, not the fund's.
|
|
381
|
+
...(w.owner?.object && w.owner?.level ? { owner: { object: String(w.owner.object), level: String(w.owner.level) } } : {}),
|
|
382
|
+
} : undefined;
|
|
383
|
+
if (w && !whose) findings.push("whose names its object, its column and its level");
|
|
384
|
+
for (const col of [whose?.column, whose?.part?.column].filter(Boolean) as string[]) if (!seen.has(col)) findings.push(`whose names column "${col}", which is not a column`);
|
|
385
|
+
if (findings.length) throw new Error(`object ${name}: ${findings.join("; ")} (${dir})`);
|
|
386
|
+
return { columns, ...(uniqueBy ? { uniqueBy } : {}), ...(file.description ? { description: String(file.description) } : {}), ...(whose ? { whose } : {}) };
|
|
299
387
|
}
|
|
300
388
|
|
|
301
389
|
function readFolder(dir: string, name: string, org: string | undefined, seen: string[]): ObjectDefinition {
|
|
@@ -382,7 +470,7 @@ function readFolder(dir: string, name: string, org: string | undefined, seen: st
|
|
|
382
470
|
...(typeof envelope.element === "string" && envelope.element ? { element: envelope.element } : parent?.element ? { element: parent.element } : {}),
|
|
383
471
|
...(envelope.meta ? { meta: envelope.meta } : parent?.meta ? { meta: parent.meta } : {}),
|
|
384
472
|
...(links && Object.keys(links).length ? { links } : {}),
|
|
385
|
-
...(envelope.matching ? { matching: envelope.matching as ObjectMatching } : parent?.matching ? { matching: parent.matching } : {}),
|
|
473
|
+
...(envelope.matching ? { matching: { ...(envelope.matching as ObjectMatching), ...((envelope.extract as ObjectDefinition["extract"])?.never?.length ? { never: (envelope.extract as ObjectDefinition["extract"])!.never } : {}) } } : parent?.matching ? { matching: parent.matching } : {}),
|
|
386
474
|
...(envelope.extract ? { extract: envelope.extract as ObjectDefinition["extract"] } : {}),
|
|
387
475
|
...(dataset ? { dataset } : {}),
|
|
388
476
|
props,
|
|
@@ -79,7 +79,17 @@ export interface MakeEntry {
|
|
|
79
79
|
* object's input props, in place of a model reading every page. Its config is resolved over the
|
|
80
80
|
* run's `config` when the rows are collected.
|
|
81
81
|
*/
|
|
82
|
-
read?: { node: string; config: Record<string, unknown>; chunks?: number | "all" };
|
|
82
|
+
read?: { node: string; config: Record<string, unknown>; chunks?: number | "all"; cacheable?: true };
|
|
83
|
+
/**
|
|
84
|
+
* READ FROM THE DOCUMENT'S TABLES TOO (pipelines/tables.md, 2026-10-12): a table's labels are
|
|
85
|
+
* matched to the object's props by their `wordings`, and a matched cell's value is copied as
|
|
86
|
+
* printed. `whose` is the node that says which of the document's rows a table is about, when
|
|
87
|
+
* the document is linked to more than one. `labels` is the node that proposes a field for a label
|
|
88
|
+
* no rule matched (column matching step 4), once per org, for a person to approve in Review.
|
|
89
|
+
*/
|
|
90
|
+
/** `allocations` names the object's line an allocation table draws, one per fund it holds, carrying the weight (tables.md 3c). */
|
|
91
|
+
/** `boards` names the object's lines a board list may draw, each to whose board it is, carrying the role (tables.md 3d). */
|
|
92
|
+
tables?: { whose?: { node: string; config: Record<string, unknown> }; subject?: boolean; labels?: { node: string; config: Record<string, unknown> }; allocations?: string; boards?: string[] };
|
|
83
93
|
sourceUrl?: string;
|
|
84
94
|
sourceId?: string;
|
|
85
95
|
/** The item's content, each value a reference over `item`, `index`, `config`, `collect`. */
|
|
@@ -162,7 +172,8 @@ function readFolder(dir: string, name: string, org: string | undefined): Pipelin
|
|
|
162
172
|
...(spec.cardinality === "one" || spec.cardinality === "many" ? { cardinality: spec.cardinality as "one" | "many" } : {}),
|
|
163
173
|
...(whenOf(spec) ? { when: whenOf(spec) } : {}),
|
|
164
174
|
...(spec.per === "page" ? { per: "page" as const } : {}),
|
|
165
|
-
...(isRecord(spec.read) && typeof spec.read.node === "string" ? { read: { node: spec.read.node, config: isRecord(spec.read.config) ? spec.read.config : {}, ...(spec.read.chunks === "all" || typeof spec.read.chunks === "number" ? { chunks: spec.read.chunks as number | "all" } : {}) } } : {}),
|
|
175
|
+
...(isRecord(spec.read) && typeof spec.read.node === "string" ? { read: { node: spec.read.node, config: isRecord(spec.read.config) ? spec.read.config : {}, ...(spec.read.chunks === "all" || typeof spec.read.chunks === "number" ? { chunks: spec.read.chunks as number | "all" } : {}), ...(spec.read.cacheable === true ? { cacheable: true as const } : {}) } } : {}),
|
|
176
|
+
...(isRecord(spec.tables) ? { tables: { ...(isRecord(spec.tables.whose) && typeof spec.tables.whose.node === "string" ? { whose: { node: spec.tables.whose.node, config: isRecord(spec.tables.whose.config) ? spec.tables.whose.config : {} } } : {}), ...(spec.tables.subject === true ? { subject: true } : {}), ...(isRecord(spec.tables.labels) && typeof spec.tables.labels.node === "string" ? { labels: { node: spec.tables.labels.node, config: isRecord(spec.tables.labels.config) ? spec.tables.labels.config : {} } } : {}), ...(typeof spec.tables.allocations === "string" && spec.tables.allocations ? { allocations: spec.tables.allocations } : {}), ...(Array.isArray(spec.tables.boards) && spec.tables.boards.length && spec.tables.boards.every((b: unknown) => typeof b === "string" && b) ? { boards: spec.tables.boards as string[] } : {}) } } : {}),
|
|
166
177
|
sourceUrl: typeof raw.sourceUrl === "string" ? raw.sourceUrl : "{{ item.address }}",
|
|
167
178
|
sourceId: typeof raw.sourceId === "string" ? raw.sourceId : "{{ item.universalId }}",
|
|
168
179
|
content: isRecord(raw.content) ? raw.content : {},
|
|
@@ -31,6 +31,8 @@ const STATUS_TEXT: Record<number, string> = {
|
|
|
31
31
|
404: "Not Found",
|
|
32
32
|
405: "Method Not Allowed",
|
|
33
33
|
406: "Not Acceptable",
|
|
34
|
+
413: "Payload Too Large",
|
|
35
|
+
429: "Too Many Requests",
|
|
34
36
|
500: "Internal Server Error",
|
|
35
37
|
};
|
|
36
38
|
const statusLine = (code: number): string => `${code} ${STATUS_TEXT[code] ?? "OK"}`;
|
|
@@ -154,9 +156,23 @@ function writeCors(res: HttpResponse): void {
|
|
|
154
156
|
res.writeHeader("Access-Control-Max-Age", "86400");
|
|
155
157
|
}
|
|
156
158
|
|
|
157
|
-
/**
|
|
158
|
-
|
|
159
|
+
/**
|
|
160
|
+
* LIMITS FOR A PUBLIC DOOR (docs/auth-security/INBOUND.md § 3). A route anyone can call says how
|
|
161
|
+
* much it will read and whether to read at all, so a flood or an oversized body is refused before
|
|
162
|
+
* it costs anything. Optional: every route without them reads as it always did.
|
|
163
|
+
*/
|
|
164
|
+
export interface FetchLimits {
|
|
165
|
+
/** The most bytes this route reads. Over it: 413, and the rest is never buffered. */
|
|
166
|
+
maxBody?: number;
|
|
167
|
+
/** Asked before a single byte is read; false is 429. The route's rate limit. */
|
|
168
|
+
admit?: (path: string) => boolean;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Buffer the full request body; resolves on the last chunk (copies as it goes), or null once it passes `maxBody`. */
|
|
172
|
+
export function readBody(res: Pick<HttpResponse, "onData">, maxBody = Infinity): Promise<Buffer | null> {
|
|
159
173
|
return new Promise((resolve) => {
|
|
174
|
+
let size = 0;
|
|
175
|
+
let over = false;
|
|
160
176
|
// Collect copies and concat ONCE at the end. The old per-chunk
|
|
161
177
|
// `Buffer.concat([buf, chunk])` reallocated+copied the whole accumulator on every
|
|
162
178
|
// chunk — O(n²) in body size, which hurts large /execute payloads. Each chunk is
|
|
@@ -167,6 +183,13 @@ function readBody(res: HttpResponse): Promise<Buffer> {
|
|
|
167
183
|
// ArrayBuffer after this callback returns. `Buffer.from(arrayBuffer)` is only a
|
|
168
184
|
// VIEW (no copy), so wrap in a Uint8Array, which Buffer.from then copies; deferring
|
|
169
185
|
// a bare view to the final concat would read detached memory.
|
|
186
|
+
if (over) return;
|
|
187
|
+
size += chunk.byteLength;
|
|
188
|
+
if (size > maxBody) {
|
|
189
|
+
over = true;
|
|
190
|
+
chunks.length = 0;
|
|
191
|
+
return resolve(null);
|
|
192
|
+
}
|
|
170
193
|
chunks.push(Buffer.from(new Uint8Array(chunk)));
|
|
171
194
|
if (isLast) resolve(chunks.length === 1 ? chunks[0] : Buffer.concat(chunks));
|
|
172
195
|
});
|
|
@@ -181,6 +204,7 @@ export async function handleFetch(
|
|
|
181
204
|
res: HttpResponse,
|
|
182
205
|
req: HttpRequest,
|
|
183
206
|
handler: (request: Request) => Promise<Response>,
|
|
207
|
+
limits: FetchLimits = {},
|
|
184
208
|
): Promise<void> {
|
|
185
209
|
const state: PumpState = { aborted: false };
|
|
186
210
|
// THE HANDLER LEARNS OF THE DISCONNECT. `Request.signal` is how a Fetch handler is told
|
|
@@ -215,8 +239,20 @@ export async function handleFetch(
|
|
|
215
239
|
return;
|
|
216
240
|
}
|
|
217
241
|
|
|
218
|
-
const
|
|
242
|
+
const refuse = (code: 413 | 429): void => {
|
|
243
|
+
res.cork(() => {
|
|
244
|
+
res.writeStatus(statusLine(code));
|
|
245
|
+
res.writeHeader("Content-Type", "application/json");
|
|
246
|
+
writeCors(res);
|
|
247
|
+
res.end(JSON.stringify({ error: code === 413 ? "too large" : "too many requests" }));
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
if (limits.admit && !limits.admit(new URL(url).pathname)) return refuse(429);
|
|
251
|
+
if (limits.maxBody !== undefined && Number(headers["content-length"] ?? 0) > limits.maxBody) return refuse(413);
|
|
252
|
+
|
|
253
|
+
const body = method !== "GET" && method !== "HEAD" ? await readBody(res, limits.maxBody) : undefined;
|
|
219
254
|
if (state.aborted) return;
|
|
255
|
+
if (body === null) return refuse(413);
|
|
220
256
|
|
|
221
257
|
let response: Response;
|
|
222
258
|
try {
|
|
@@ -91,6 +91,16 @@ function orgDesignSystemDir(dir: string): string {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/** Collect an org repo into items: the same walk as a monorepo project, rooted at the repo. */
|
|
94
|
+
/**
|
|
95
|
+
* EVERY KIND THE WALK READS from an org's folder. A sync that reads a folder with this walk
|
|
96
|
+
* knows each of these completely: none of a kind in the folder means none in the org, so the
|
|
97
|
+
* sync may remove what is gone. A kind not listed (a recipe) is never read here, so a sync
|
|
98
|
+
* never removes it.
|
|
99
|
+
*/
|
|
100
|
+
export const COLLECTED_KINDS: ReadonlySet<string> = new Set([
|
|
101
|
+
"component", "app", "template", "atom", "identity", "agent", "object", "pipeline", "style", "skill", "prompt-block", "node",
|
|
102
|
+
]);
|
|
103
|
+
|
|
94
104
|
export function collectOrg(home: OrgHome): CollectedItem[] {
|
|
95
105
|
return collectFrom(home.dir, home.org, orgDesignSystemDir(home.dir));
|
|
96
106
|
}
|
|
@@ -118,6 +118,16 @@ export async function lintOrg(home: OrgHome): Promise<{ problems: Finding[]; err
|
|
|
118
118
|
return { problems: result.problems, errors: result.problems.filter((p: Finding) => p.level === "error") };
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Lint an org repo's objects and pipelines with some files as they WOULD be (`overlay`: absolute path ->
|
|
123
|
+
* text): a change is checked before it is written (an approved table wording, pipelines/tables.md).
|
|
124
|
+
*/
|
|
125
|
+
export async function lintOrgObjects(home: OrgHome, overlay: Record<string, string> = {}): Promise<{ problems: Finding[]; errors: Finding[] }> {
|
|
126
|
+
const { lintObjects } = await import("../lint/objects/index.mjs");
|
|
127
|
+
const problems = lintObjects(home.dir, { org: { name: home.org, dir: home.dir }, overlay }).problems;
|
|
128
|
+
return { problems, errors: problems.filter((p: Finding) => p.level === "error") };
|
|
129
|
+
}
|
|
130
|
+
|
|
121
131
|
/**
|
|
122
132
|
* What publishing WOULD do, without doing it.
|
|
123
133
|
*
|
|
@@ -868,7 +868,10 @@ for (const orgDir of [DS, ...orgDirs]) {
|
|
|
868
868
|
// This was an org-blocking error until it stopped a yasisland deploy over a form
|
|
869
869
|
// nobody needed that day.
|
|
870
870
|
if (credentialDefs.size)
|
|
871
|
-
for (const
|
|
871
|
+
for (const entry of Array.isArray(readDef(mf).credentials) ? readDef(mf).credentials : [])
|
|
872
|
+
// `- googleApiCredential` or `- googleApiCredential: MapKey` (the saved name chooses
|
|
873
|
+
// between two of one type); the definition looked for is the TYPE either way.
|
|
874
|
+
for (const cname of typeof entry === "string" ? [entry] : entry && typeof entry === "object" ? Object.keys(entry) : [String(entry)])
|
|
872
875
|
if (!credentialDefs.has(String(cname)))
|
|
873
876
|
report(
|
|
874
877
|
"warn",
|
|
@@ -72,7 +72,7 @@ export const INTERFACE_FIELDS = new Set([
|
|
|
72
72
|
export const AUTHORED_CONTROLS = ["maxLength", "minItems", "maxItems", "enum"];
|
|
73
73
|
|
|
74
74
|
export const PRIMITIVE_PROPS = {
|
|
75
|
-
Box: ["children", "autoScroll", "revealOnStick", "selection", "appWidth"],
|
|
75
|
+
Box: ["children", "autoScroll", "revealOnStick", "selection", "appWidth", "pan"],
|
|
76
76
|
Stack: ["children", "autoScroll", "revealOnStick", "appWidth"],
|
|
77
77
|
Row: ["children", "autoScroll", "revealOnStick", "appWidth"],
|
|
78
78
|
Column: ["children", "autoScroll", "revealOnStick", "appWidth"],
|
|
@@ -22,7 +22,7 @@ import { problems, reset, report, state } from "../context.mjs";
|
|
|
22
22
|
import { validateAgainst, Validator } from "../nodes/schema.mjs";
|
|
23
23
|
import {
|
|
24
24
|
envelopeFindings, propsFindings, fromFindings, serviceFindings, makeFindings, rowKeyFindings, objectTestFindings,
|
|
25
|
-
pipelineFindings, pipelineFolderFindings,
|
|
25
|
+
pipelineFindings, pipelineFolderFindings, layoutFindings,
|
|
26
26
|
} from "./rules.mjs";
|
|
27
27
|
|
|
28
28
|
const require_ = createRequire(import.meta.url);
|
|
@@ -31,6 +31,9 @@ export function objectTestFindings(name: string, test: unknown, service: unknown
|
|
|
31
31
|
/** The words a `make` block says about its read; every other key is an object that read makes. */
|
|
32
32
|
export const PIPELINE_READ_KEYS: Set<string>;
|
|
33
33
|
export function pipelineFindings(name: string, file: unknown, fileName?: string): RuleFinding[];
|
|
34
|
+
/** A layout names no document (document-map.md, 2026-10-21): refuses a fund's, a firm's or a ticker's name, and lists of names. */
|
|
35
|
+
export function layoutFindings(name: string, file: unknown, fileName?: string): RuleFinding[];
|
|
36
|
+
export const LAYOUT_VOCABULARY: Set<string>;
|
|
34
37
|
export function collectRefs(value: unknown): Set<string>;
|
|
35
38
|
export function configRefs(value: unknown): Set<string>;
|
|
36
39
|
export function pipelineFolderFindings(
|
|
@@ -29,7 +29,9 @@ export const ELEMENTS = new Set(["purpose", "story", "content", "capability", "p
|
|
|
29
29
|
export const FACE_KEYS = ["title", "description", "whenToUse", "category", "version"];
|
|
30
30
|
|
|
31
31
|
/** The prop grammar of UNOVERSE_AUTHORING 3b plus the object's one word, `from` (README 4c). */
|
|
32
|
-
|
|
32
|
+
// `none` IS NO DEFAULT (2026-10-15): it is what a cell that STATES there is none ("None", "N/A") stands for, so a
|
|
33
|
+
// stated "no performance fee" is 0 and a fee nobody states stays empty.
|
|
34
|
+
export const PROP_KEYS = new Set(["type", "input", "description", "from", "hydrate", "enum", "oneOf", "minLength", "maxLength", "minItems", "maxItems", "items", "reuse", "answeredBy", "standard", "wordings", "wordingsIn", "onlyWith", "format", "pattern", "none"]);
|
|
33
35
|
export const LIMIT_KEYS = ["minLength", "maxLength", "minItems", "maxItems"];
|
|
34
36
|
|
|
35
37
|
/** Words that left the object grammar and must not come back (README 11.7, 12.3, 12.4). */
|
|
@@ -96,7 +98,9 @@ export function envelopeFindings(name, envelope, file = `${name}.yaml`) {
|
|
|
96
98
|
if (pairs.length !== 1 || pairs.some(([a, b]) => typeof a !== "string" || typeof b !== "string" || !a || !b)) refuse(`object ${name}: link ${ln}: match is one pair, this row's field naming that row's ({ manager: legalName })`);
|
|
97
99
|
if (l.between != null || l.order != null) refuse(`object ${name}: link ${ln}: a line to another object says to and match, never between or order`);
|
|
98
100
|
if (l.alike != null && (!Array.isArray(l.alike) || l.alike.some((f) => typeof f !== "string" || !pairs.some(([a]) => a === f)))) refuse(`object ${name}: link ${ln}: alike lists this row's match field whose value may name one thing several ways`);
|
|
99
|
-
|
|
101
|
+
// WHAT A LINE CARRIES (relationships-and-labels.md, 2026-10-14) is fields of this row, by name.
|
|
102
|
+
if (l.carries != null && (!Array.isArray(l.carries) || l.carries.some((f) => typeof f !== "string" || !f))) refuse(`object ${name}: link ${ln}: carries lists fields of this row each line keeps ([role])`);
|
|
103
|
+
for (const k of Object.keys(l)) if (!["type", "to", "match", "when", "alike", "carries"].includes(k)) say(`object ${name}: link ${ln}: "${k}" is not a word here`);
|
|
100
104
|
continue;
|
|
101
105
|
}
|
|
102
106
|
if (!Array.isArray(l.between) || !l.between.length || l.between.some((f) => typeof f !== "string")) refuse(`object ${name}: link ${ln}: between is a list of the fields whose equal values make one line of rows`);
|
|
@@ -116,7 +120,9 @@ export function envelopeFindings(name, envelope, file = `${name}.yaml`) {
|
|
|
116
120
|
for (const k of ["ids", "evidence", "ruleOut"]) if (m[k] != null && !fields(m[k])) refuse(`object ${name}: matching: ${k} is a list of field names`);
|
|
117
121
|
if (m.createFrom != null && !fields(m.createFrom)) refuse(`object ${name}: matching: createFrom is a list of line names (about)`);
|
|
118
122
|
if (m.remember != null && (typeof m.remember !== "string" || !(m.names ?? []).includes(m.remember))) refuse(`object ${name}: matching: remember names one of names, the list a confirmed name is added to`);
|
|
119
|
-
|
|
123
|
+
// A JUDGE IS A NODE BY ITS TYPE, OR A DECISION NODE WITH ITS CONFIG as `review` is ({ node: OpenAIDecision, config: { model } }).
|
|
124
|
+
const judgeNode = isRecord(m.judge) ? m.judge.node : m.judge;
|
|
125
|
+
if (m.judge != null && (typeof judgeNode !== "string" || !/^[A-Z][A-Za-z0-9]*$/.test(judgeNode) || (isRecord(m.judge) && m.judge.config != null && !isRecord(m.judge.config)))) refuse(`object ${name}: matching: judge names a node by its type (TypeSafeChoice), or a decision node with its config ({ node: OpenAIDecision, config: { model } })`);
|
|
120
126
|
for (const k of Object.keys(m)) if (!["ids", "names", "remember", "evidence", "ruleOut", "judge", "link", "review", "createFrom"].includes(k)) say(`object ${name}: matching: "${k}" is not a word here`);
|
|
121
127
|
}
|
|
122
128
|
}
|
|
@@ -160,6 +166,20 @@ export function propsFindings(name, ownProps, parentProps = null, base = null, f
|
|
|
160
166
|
if (raw.type === "array" && !isRecord(raw.items)) say(`object ${name}: prop "${k}" is an array and carries no items: an array says the shape of one element, in the same grammar one level down (README 4c)`);
|
|
161
167
|
if (raw.answeredBy != null && (!Array.isArray(raw.answeredBy) || !raw.answeredBy.length || raw.answeredBy.some((d) => typeof d !== "string" || !d.trim()))) say(`object ${name}: prop "${k}": answeredBy is a list of the document kinds that may answer it, e.g. [offering-memorandum, side-letter]`);
|
|
162
168
|
if (raw.answeredBy != null && raw.input !== true) say(`object ${name}: prop "${k}" says answeredBy but not input: true; only a field read from documents is answered by one`);
|
|
169
|
+
// A FIELD A TABLE CAN FILL (pipelines/tables.md, 2026-10-12): `standard` is its id in the org's
|
|
170
|
+
// field list, `wordings` the words documents print for it, matched exactly after lower case.
|
|
171
|
+
if (raw.standard != null && (typeof raw.standard !== "string" || !raw.standard.trim())) say(`object ${name}: prop "${k}": standard is the field's id in the org's field list, one word (OFST452010)`);
|
|
172
|
+
if (raw.wordings != null && (!Array.isArray(raw.wordings) || !raw.wordings.length || raw.wordings.some((w) => typeof w !== "string" || !w.trim()))) say(`object ${name}: prop "${k}": wordings is a list of the words documents print for this field, e.g. [management fee, mgmt fee]`);
|
|
173
|
+
if (raw.pattern != null) { try { if (typeof raw.pattern !== "string" || !raw.pattern.trim()) throw 0; new RegExp(raw.pattern); } catch { say(`object ${name}: prop "${k}": pattern is the regular expression a value must match, e.g. ^[A-Z]{2}[A-Z0-9]{9}[0-9]$; a value that does not match is never saved`); } }
|
|
174
|
+
if (raw.format != null && raw.format !== "date" && raw.format !== "country") say(`object ${name}: prop "${k}": format is date, country, or nothing (a date is written yyyy-mm-dd; a country is held by its one name from ISO 3166, "UK" saved as United Kingdom)`);
|
|
175
|
+
// A WORDING SCOPED TO ONE KIND OF DOCUMENT (2026-10-26): `wordingsIn` lists, under a document type, the words that name
|
|
176
|
+
// this field in that type only, where one type prints for this field the words another prints for another field.
|
|
177
|
+
if (raw.wordingsIn != null && (!isRecord(raw.wordingsIn) || !Object.keys(raw.wordingsIn).length || Object.values(raw.wordingsIn).some((ws) => !Array.isArray(ws) || !ws.length || ws.some((w) => typeof w !== "string" || !w.trim())))) say(`object ${name}: prop "${k}": wordingsIn lists, under each document type, the words that name this field in that type only, as { <document type>: [<words>] }`);
|
|
178
|
+
if (raw.wordingsIn != null && raw.input !== true) say(`object ${name}: prop "${k}" says wordingsIn but not input: true; only a field read from documents is filled from a table`);
|
|
179
|
+
// A TERM OF ANOTHER FIELD (2026-10-26): `onlyWith` names the field this one is a term of; where that field is none,
|
|
180
|
+
// this one is not kept.
|
|
181
|
+
if (raw.onlyWith != null && (typeof raw.onlyWith !== "string" || raw.onlyWith === k || !(raw.onlyWith in ownProps || (parentProps && raw.onlyWith in parentProps)))) say(`object ${name}: prop "${k}": onlyWith names another field of this object that this one is a term of, and is kept only where that field is not none`);
|
|
182
|
+
if ((raw.standard != null || raw.wordings != null) && raw.input !== true) say(`object ${name}: prop "${k}" says standard or wordings but not input: true; only a field read from documents is filled from a table`);
|
|
163
183
|
}
|
|
164
184
|
// FILLED FROM DOCUMENTS, EVERY FIELD SAYS WHICH (proved-before-built.md, ruled 2026-10-07): an
|
|
165
185
|
// object one of whose fields names the documents that answer it is filled from documents, so
|
|
@@ -313,7 +333,7 @@ export const PIPELINE_READ_KEYS = new Set(["each", "sourceUrl", "sourceId", "con
|
|
|
313
333
|
const DROPPED_MAKE_KEYS = ["partsOf", "by", "under", "when", "from", "any", "index"];
|
|
314
334
|
|
|
315
335
|
/** What an object may say: under `each`, how many and what it holds; made once per pack, how many and from which rows. */
|
|
316
|
-
const ITEM_OBJECT_KEYS = new Set(["cardinality", "any", "parts", "where", "per", "read"]);
|
|
336
|
+
const ITEM_OBJECT_KEYS = new Set(["cardinality", "any", "parts", "where", "per", "read", "tables"]);
|
|
317
337
|
const FIELD = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
318
338
|
|
|
319
339
|
/**
|
|
@@ -390,7 +410,11 @@ export function pipelineFindings(name, file, fileName = "pipeline.yaml") {
|
|
|
390
410
|
}
|
|
391
411
|
if (made.includes(o)) refuse(`pipeline ${name}: ${at}.${o} is made above already; one entry makes an object`);
|
|
392
412
|
const allowed = ITEM_OBJECT_KEYS;
|
|
393
|
-
for (const k of Object.keys(spec)) if (!allowed.has(k)) refuse(`pipeline ${name}: ${at}.${o}: "${k}" is not a word here; an object says cardinality, any, parts, where, per and
|
|
413
|
+
for (const k of Object.keys(spec)) if (!allowed.has(k)) refuse(`pipeline ${name}: ${at}.${o}: "${k}" is not a word here; an object says cardinality, any, parts, where, per, read and tables (${see})`);
|
|
414
|
+
if ("tables" in spec && (!isRecord(spec.tables) || ("whose" in spec.tables && (!isRecord(spec.tables.whose) || typeof spec.tables.whose.node !== "string" || !spec.tables.whose.node)))) refuse(`pipeline ${name}: ${at}.${o}: tables is a record; its whose names the node that says which row a table is about: tables: { whose: { node: OpenAIDecision, config: { ... } } } (pipelines/tables.md)`);
|
|
415
|
+
if (isRecord(spec.tables) && "labels" in spec.tables && (!isRecord(spec.tables.labels) || typeof spec.tables.labels.node !== "string" || !spec.tables.labels.node)) refuse(`pipeline ${name}: ${at}.${o}: tables.labels names the node that proposes a field for a label no rule matched: tables: { labels: { node: OpenAIDecision, config: { ... } } } (pipelines/tables.md)`);
|
|
416
|
+
if (isRecord(spec.tables) && "allocations" in spec.tables && (typeof spec.tables.allocations !== "string" || !spec.tables.allocations)) refuse(`pipeline ${name}: ${at}.${o}: tables.allocations names the line an allocation table draws to each fund it holds: tables: { allocations: allocatesTo } (pipelines/tables.md 3c)`);
|
|
417
|
+
if (isRecord(spec.tables) && "boards" in spec.tables && (!Array.isArray(spec.tables.boards) || !spec.tables.boards.length || !spec.tables.boards.every((b) => typeof b === "string" && b))) refuse(`pipeline ${name}: ${at}.${o}: tables.boards names the lines a board list may draw, each to whose board it is: tables: { boards: [directorOf, worksAt] } (pipelines/tables.md 3d)`);
|
|
394
418
|
if ("read" in spec && (!isRecord(spec.read) || typeof spec.read.node !== "string" || !spec.read.node)) refuse(`pipeline ${name}: ${at}.${o}: read names the node that reads the pages: read: { node: BeamEndpoint, config: { ... } } (${see})`);
|
|
395
419
|
if ("read" in spec && spec.per !== "page") refuse(`pipeline ${name}: ${at}.${o}: read is how an object written per page is read; say per: page beside it (${see})`);
|
|
396
420
|
if ("per" in spec && spec.per !== "page") refuse(`pipeline ${name}: ${at}.${o}: per says page, the only unit an object is written per; there are no sections (page-store.md) (${see})`);
|
|
@@ -467,6 +491,70 @@ export function pipelineFolderFindings(name, { file, config, test }, { nodeTypes
|
|
|
467
491
|
}
|
|
468
492
|
|
|
469
493
|
/** The first error's message. */
|
|
494
|
+
// ── Layouts ─────────────────────────────────────────────────────────────────────────────────────────
|
|
495
|
+
/**
|
|
496
|
+
* A LAYOUT NAMES NO DOCUMENT (docs/unoverse/data/pipelines/document-map.md, owner 2026-10-21, seeing a
|
|
497
|
+
* draft carry "EAM B.V." and "TL-X"). A layout is the map of a document TYPE: it speaks in the type's
|
|
498
|
+
* structure and wording, never in a fund's, a firm's, an index's or a ticker's name. Names belong on
|
|
499
|
+
* records. Refused here, so a layout that fits one house's documents is never loaded.
|
|
500
|
+
*/
|
|
501
|
+
/** A legal form as one word of a name, in any jurisdiction's spelling: the mark of a firm's or a fund's name. */
|
|
502
|
+
const LEGAL_FORM_WORD = /^(?:Ltd|Limited|LLC|LLP|L\.?P\.?|LP|plc|PLC|Inc\.?|Corp\.?|Co\.?|GmbH|AG|S\.?A\.?|S\.?C\.?A\.?|S\.?p\.?A\.?|SpA|B\.?V\.?|BV|N\.?V\.?|NV|S\.?à\.?r\.?l\.?|SARL|Sàrl|Pte\.?|Pty\.?|KGaA|KG|ApS|AB|AS|Oy|SE|SAS|SL|SRL|Srl|V)\.?$/;
|
|
503
|
+
const LAYOUT_TEXT_KEYS = new Set(["pattern", "after", "heading", "text", "row", "table", "say", "describe", "trim", "split"]);
|
|
504
|
+
const LAYOUT_NAME_KEYS = new Set(["names", "sameAs", "neverMerge", "aliases"]);
|
|
505
|
+
/** Every capitalised run of two or more words in a string, regex syntax and escapes aside. */
|
|
506
|
+
function capitalisedRuns(str) {
|
|
507
|
+
// Regex syntax is not words: character classes, counts, escapes and group openers go before the words are read.
|
|
508
|
+
const plain = String(str).replace(/\(\?<?[!=][^)]*\)/g, " ").replace(/\[[^\]]*\]/g, " ").replace(/\{[^}]*\}/g, " ").replace(/\\[A-Za-z]/g, " ").replace(/\(\?[:!=<]?/g, " ").replace(/[\[\]()|^$?*+{}.\\]/g, " ").replace(/\s+/g, " ");
|
|
509
|
+
return [...plain.matchAll(/(?:\b[A-Z][\w'&.-]*(?: (?:of|de|di|du|des|and|&) )?\s?){2,}/g)].map((m) => m[0].trim()).filter((r) => r.split(/\s+/).length >= 2);
|
|
510
|
+
}
|
|
511
|
+
/** A ticker or an identifier spelled out: letters and digits run together ("LU0000000000", "ABCDEF LX", "X1AA"), or an ISIN shape. */
|
|
512
|
+
const LITERAL_CODE = /\b(?:[A-Z]{2}[A-Z0-9]{9}\d|[A-Z]{4,}\s[A-Z]{2}|[A-Z]+\d+[A-Z]+|TL-[A-Z]\d*)\b/;
|
|
513
|
+
/**
|
|
514
|
+
* The same inside a finder that reads any case (`case: any`, 2026-10-24): there capitals are the type's wording as printed
|
|
515
|
+
* ("A SUB-FUND OF THE"), so a word of four capitals then one of two is a ticker only when the two stand alone ("ABCDEF LX");
|
|
516
|
+
* a code with digits or an identifier's shape is refused as everywhere.
|
|
517
|
+
*/
|
|
518
|
+
const LITERAL_CODE_ANY_CASE = /\b(?:[A-Z]{2}[A-Z0-9]{9}\d|[A-Z]+\d+[A-Z]+|TL-[A-Z]\d*)\b|(?<![A-Za-z][\s-]?)\b[A-Z]{4,}\s[A-Z]{2}\b(?![\s-]?[A-Za-z])/;
|
|
519
|
+
export function layoutFindings(name, file, fileName = `${name}.yaml`) {
|
|
520
|
+
const out = [];
|
|
521
|
+
const refuse = (msg) => out.push({ file: fileName, msg, refuses: true });
|
|
522
|
+
if (!isRecord(file)) return [{ file: fileName, msg: `layout ${name}: ${fileName} must be a record`, refuses: true }];
|
|
523
|
+
const walk = (v, path, anyCase = false) => {
|
|
524
|
+
if (Array.isArray(v)) { v.forEach((x, i) => walk(x, `${path}[${i}]`, anyCase)); return; }
|
|
525
|
+
if (isRecord(v)) {
|
|
526
|
+
// A FINDER THAT READS ANY CASE (`case: any`): its own strings are wording, capitals and all; a record beneath it is its own.
|
|
527
|
+
const own = v.case === "any";
|
|
528
|
+
for (const [k, x] of Object.entries(v)) {
|
|
529
|
+
if (LAYOUT_NAME_KEYS.has(k)) refuse(`layout ${name}: "${k}" at ${path} lists names; a layout names no document, aliases live on records (document-map.md, 2026-10-21)`);
|
|
530
|
+
else if (k === "map" && isRecord(x)) { for (const [from, to] of Object.entries(x)) for (const t of [from, String(to)]) check(t, `${path}.map`); }
|
|
531
|
+
else walk(x, path ? `${path}.${k}` : k, own && typeof x === "string");
|
|
532
|
+
}
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (typeof v === "string") { const key = path.split(/[.[]/).pop()?.replace("]", ""); if (LAYOUT_TEXT_KEYS.has(key)) check(v, path, anyCase); }
|
|
536
|
+
};
|
|
537
|
+
const check = (str, path, anyCase = false) => {
|
|
538
|
+
// BY STRUCTURE, NEVER BY A WORD LIST (owner 2026-10-24: a list of a document type's words grew from one org's documents).
|
|
539
|
+
// A name shows itself two ways: a capitalised run that ends in a legal form and holds a word that is not one
|
|
540
|
+
// ("Example Asset Management B.V."; an alternation of legal forms alone is pattern syntax), and a `never` pattern that
|
|
541
|
+
// is plain words with no regular-expression structure, which can only be one name. Wording a pattern matches or maps
|
|
542
|
+
// ("No Distribution", "Swiss Paying Agent") is the type's own and carries no name.
|
|
543
|
+
const plain = String(str).replace(/\\[A-Za-z]/g, " ");
|
|
544
|
+
for (const run of capitalisedRuns(plain)) {
|
|
545
|
+
const words = run.split(/\s+/).filter(Boolean);
|
|
546
|
+
const last = words[words.length - 1] ?? "";
|
|
547
|
+
// A lone capital letter is a legal form's initial once its dots are gone ("S A", "B V"), never a word of a name.
|
|
548
|
+
const ownWords = words.filter((w) => !LEGAL_FORM_WORD.test(w) && !/^[A-Z]$/.test(w) && !/^(?:of|de|di|du|des|and|&)$/i.test(w));
|
|
549
|
+
if (LEGAL_FORM_WORD.test(last) && ownWords.length >= 1) refuse(`layout ${name}: "${run}" at ${path} reads like a name (a legal form ends it); a layout names no document (document-map.md, 2026-10-21)`);
|
|
550
|
+
}
|
|
551
|
+
if (/(^|\.)never\[\d+\]\.pattern$/.test(path) && !/[\\^$.*+?()[\]{}|]/.test(str) && /[A-Z]/.test(str)) refuse(`layout ${name}: "${str.slice(0, 60)}" at ${path} reads like a name (plain words with no structure); a never rule is structural (document-map.md, 2026-10-21)`);
|
|
552
|
+
if ((anyCase ? LITERAL_CODE_ANY_CASE : LITERAL_CODE).test(str.replace(/\[[^\]]*\]|\{[^}]*\}|\\[A-Za-z]/g, " "))) refuse(`layout ${name}: "${str.slice(0, 60)}" at ${path} spells out a code or ticker; a layout names no document (document-map.md, 2026-10-21)`);
|
|
553
|
+
};
|
|
554
|
+
walk(file, "");
|
|
555
|
+
return out;
|
|
556
|
+
}
|
|
557
|
+
|
|
470
558
|
export function firstError(findings) {
|
|
471
559
|
const f = findings.find((x) => !x.level || x.level === "error");
|
|
472
560
|
return f ? f.msg : null;
|
|
@@ -586,6 +586,9 @@ export function composeNode(raw: RawNode, pkg: RawPackage): ComposedNode {
|
|
|
586
586
|
attaches: iface.attaches ?? null,
|
|
587
587
|
visibility: node.visibility ?? "public",
|
|
588
588
|
capabilities: node.capabilities ?? null,
|
|
589
|
+
// HOW THE SERVICE PROVES A MESSAGE IS ITS OWN (docs/auth-security/INBOUND.md § 2 step 3): a
|
|
590
|
+
// scheme the platform verifies, and the credential field holding the secret. Null: it sends none.
|
|
591
|
+
inbound: node.inbound ?? null,
|
|
589
592
|
inputs: iface.inputs ?? [],
|
|
590
593
|
outputs: iface.outputs ?? [],
|
|
591
594
|
credentials: iface.credentials ?? [],
|
|
@@ -6,7 +6,7 @@ import { contextFor } from "./context.js";
|
|
|
6
6
|
import { sessionFor, stateStoreFor, audioLaneFor } from "./session.js";
|
|
7
7
|
import { refireAfterDocMutation } from "./refire.js";
|
|
8
8
|
import { toolBridgeFor } from "./toolBridge.js";
|
|
9
|
-
import { hydrateMemory, ingestMemoryTurn, sessionTurnRecorder } from "./memory.js";
|
|
9
|
+
import { hydrateMemory, hydrateOpenedTask, ingestMemoryTurn, sessionTurnRecorder } from "./memory.js";
|
|
10
10
|
|
|
11
11
|
export class ManifestCallbackExecutor {
|
|
12
12
|
logger: any = console;
|
|
@@ -69,7 +69,8 @@ export class ManifestCallbackExecutor {
|
|
|
69
69
|
// A SESSION is a run that holds a socket for the length of a call: the block rides the
|
|
70
70
|
// replayed history instead of a prompt, and turns are saved as they complete (below).
|
|
71
71
|
const session = Array.isArray(node.api?.run) && node.api.run.some((r: any) => r?.transport === "ws");
|
|
72
|
-
const
|
|
72
|
+
const memoryConfig = await hydrateMemory(event.config, executionContext ?? this.executionContext, { session });
|
|
73
|
+
const hydratedConfig = session ? await hydrateOpenedTask(memoryConfig, executionContext ?? this.executionContext) : memoryConfig;
|
|
73
74
|
const ctx = contextFor(node, event.inputs, hydratedConfig, executionContext ?? this.executionContext);
|
|
74
75
|
const turns = session ? sessionTurnRecorder(node.type, event.config, executionContext ?? this.executionContext) : null;
|
|
75
76
|
|