@unoverse-platform/studio 0.1.112 → 0.1.114
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 +2 -3
- package/local/plugin.ts +2 -2
- package/package.json +1 -1
- package/vendor/base/src/definitions/load/load.ts +1 -1
- package/vendor/base/src/definitions/model.ts +1 -1
- package/vendor/base/src/definitions/registry/objects.ts +30 -0
- package/vendor/base/src/definitions/registry/pipelines.ts +8 -1
- package/vendor/base/src/items/collect.ts +1 -1
- package/vendor/base/src/lint/design/file.mjs +2 -2
- package/vendor/base/src/lint/objects/rules.mjs +26 -4
- package/vendor/base/src/workflow/PlatformMemoryCore.ts +2 -2
- package/vendor/sdk/appEngine.ts +22 -0
- package/vendor/sdk/lib/dom/component.ts +8 -3
- package/vendor/sdk/lib/dom/primitives.ts +6 -2
- package/vendor/sdk/pageInterfaces.ts +12 -4
package/README.md
CHANGED
|
@@ -54,9 +54,8 @@ full client:
|
|
|
54
54
|
its own; the renderer loads when required, from the universe that owns it.
|
|
55
55
|
- **Publish**: every universe pulls the org's repo; nothing is pushed in. Connect the
|
|
56
56
|
repo on the org page's Source tab, and a push arrives by webhook within seconds, by
|
|
57
|
-
polling within 5 minutes, or with Sync now.
|
|
58
|
-
|
|
59
|
-
first; nothing leaves your machine until the rules pass.
|
|
57
|
+
polling within 5 minutes, or with Sync now. The universe lints
|
|
58
|
+
each commit first and applies nothing that fails.
|
|
60
59
|
|
|
61
60
|
The universe is somebody's deployment of the Unoverse platform. Yours, your team's, or
|
|
62
61
|
a customer's. Studio never runs one.
|
package/local/plugin.ts
CHANGED
|
@@ -364,8 +364,8 @@ export function localStudioHost(): Plugin {
|
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
/**
|
|
367
|
-
* THE WORKSPACE'S OWN UNIVERSE, for the Live preview. unoverse.yaml (
|
|
368
|
-
*
|
|
367
|
+
* THE WORKSPACE'S OWN UNIVERSE, for the Live preview. unoverse.yaml (committed
|
|
368
|
+
* with the repo) names where this
|
|
369
369
|
* workspace ships; failing that, a machine signed in to exactly one universe
|
|
370
370
|
* has answered the question too. This is the "future work" liveUniverseUrl's
|
|
371
371
|
* stub promised: the universe comes from the workspace, never a browser
|
package/package.json
CHANGED
|
@@ -184,7 +184,7 @@ export function loadDefinition(
|
|
|
184
184
|
if (m.preview && typeof m.preview === "object") def.preview = m.preview as Record<string, string[]>;
|
|
185
185
|
// THE WORKFLOW THAT WORKS IT (UNOVERSE_HOST_PAGE.md rule 4): the manifest names it,
|
|
186
186
|
// and the page reads it here to start a call on it.
|
|
187
|
-
if (m.binding && typeof m.binding === "object") def.binding = m.binding as { workflow?: string; trigger?: string };
|
|
187
|
+
if (m.binding && typeof m.binding === "object") def.binding = m.binding as { workflow?: string; trigger?: string; pipeline?: string };
|
|
188
188
|
// SPATIAL-CAPABLE, exactly as a component is (§9c, ruled 2026-08-30): a
|
|
189
189
|
// template is found by what a guest says, through the same projection, the
|
|
190
190
|
// same registry switch and the same embedded text. The difference is only
|
|
@@ -52,7 +52,7 @@ export interface UnoverseDefinition {
|
|
|
52
52
|
/** TEMPLATES: the workflow that works this template as a task, from its manifest.yaml
|
|
53
53
|
* (docs/unoverse/design/sdk/UNOVERSE_HOST_PAGE.md rule 4). A page starts a call on it from
|
|
54
54
|
* the avatar; a template without one starts nothing. */
|
|
55
|
-
binding?: { workflow?: string; trigger?: string };
|
|
55
|
+
binding?: { workflow?: string; trigger?: string; pipeline?: string };
|
|
56
56
|
/** Source basename for FILE definitions (atoms) — the identity refs resolve by. */
|
|
57
57
|
file?: string;
|
|
58
58
|
/** The client org it belongs to (server-injected from its folder, never
|
|
@@ -96,6 +96,28 @@ export interface ObjectLink {
|
|
|
96
96
|
alike?: string[];
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* HOW A NAME IS MATCHED TO A ROW OF THIS OBJECT (docs/unoverse/data/objects/entity-resolution.md,
|
|
101
|
+
* "How an org declares it", agreed 2026-10-05). The words are the org's; the steps are the
|
|
102
|
+
* platform's (services/ingestion/pipeline/5-save/matching.ts).
|
|
103
|
+
*/
|
|
104
|
+
export interface ObjectMatching {
|
|
105
|
+
/** Equal on both rows, compared as words: the same thing. Decides alone. */
|
|
106
|
+
ids?: string[];
|
|
107
|
+
/** The fields that hold its names; a mention equal to any of them, as words, is that row. */
|
|
108
|
+
names: string[];
|
|
109
|
+
/** One of `names`, a list: where a confirmed name is added. */
|
|
110
|
+
remember?: string;
|
|
111
|
+
/** Shown to the judge beside each candidate. */
|
|
112
|
+
evidence?: string[];
|
|
113
|
+
/** Filled on both and different, as words: never the same thing. */
|
|
114
|
+
ruleOut?: string[];
|
|
115
|
+
/** The node that decides a name the rules leave open (TypeSafeChoice). Absent, an open name goes to review. */
|
|
116
|
+
judge?: string;
|
|
117
|
+
/** 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. */
|
|
118
|
+
createFrom?: string[];
|
|
119
|
+
}
|
|
120
|
+
|
|
99
121
|
/**
|
|
100
122
|
* A COLUMN OF A DATASET (dataset.md, "the declaration is the schema"). The same words a prop
|
|
101
123
|
* uses, so an author learns one grammar: a name, a type, whether it must be there, and whether
|
|
@@ -182,6 +204,8 @@ export interface ObjectDefinition {
|
|
|
182
204
|
meta?: "open";
|
|
183
205
|
/** The lines it draws between its own rows, by name (README 7; relationships-and-labels.md). */
|
|
184
206
|
links?: Record<string, ObjectLink>;
|
|
207
|
+
/** How a name is matched to its rows (entity-resolution.md); absent, a line to it compares one field with one field. */
|
|
208
|
+
matching?: ObjectMatching;
|
|
185
209
|
/** THE TABLE THIS OBJECT OWNS (docs/unoverse/data/objects/dataset.md, PROPOSED 2026-09-18),
|
|
186
210
|
* from its folder's table.yaml: an object that holds many small facts read together rather
|
|
187
211
|
* than one row read alone. Absent on every ordinary object, which stays a row. */
|
|
@@ -325,6 +349,11 @@ function readFolder(dir: string, name: string, org: string | undefined, seen: st
|
|
|
325
349
|
for (const f of mine) if (typeof f === "string" && !(f in props)) throw new Error(`object ${name}: link ${ln} names the field "${f}", which the object does not have (${dir})`);
|
|
326
350
|
for (const f of l?.alike ?? []) if (!mine.includes(f)) throw new Error(`object ${name}: link ${ln}: alike names "${f}", which is not one of its ${l?.to ? "match" : "between"} fields (${dir})`);
|
|
327
351
|
}
|
|
352
|
+
// ITS MATCHING names only its own fields, for the same reason.
|
|
353
|
+
const matching = (envelope.matching ?? parent?.matching) as ObjectMatching | undefined;
|
|
354
|
+
for (const f of [...(matching?.ids ?? []), ...(matching?.names ?? []), ...(matching?.evidence ?? []), ...(matching?.ruleOut ?? [])]) {
|
|
355
|
+
if (!(f in props) && !["title", "description", "source_url", "source_id"].includes(f)) throw new Error(`object ${name}: matching names the field "${f}", which the object does not have (${dir})`);
|
|
356
|
+
}
|
|
328
357
|
return {
|
|
329
358
|
name,
|
|
330
359
|
...(org ? { org } : {}),
|
|
@@ -338,6 +367,7 @@ function readFolder(dir: string, name: string, org: string | undefined, seen: st
|
|
|
338
367
|
...(typeof envelope.element === "string" && envelope.element ? { element: envelope.element } : parent?.element ? { element: parent.element } : {}),
|
|
339
368
|
...(envelope.meta ? { meta: envelope.meta } : parent?.meta ? { meta: parent.meta } : {}),
|
|
340
369
|
...(links && Object.keys(links).length ? { links } : {}),
|
|
370
|
+
...(envelope.matching ? { matching: envelope.matching as ObjectMatching } : parent?.matching ? { matching: parent.matching } : {}),
|
|
341
371
|
...(dataset ? { dataset } : {}),
|
|
342
372
|
props,
|
|
343
373
|
ownProps,
|
|
@@ -31,7 +31,7 @@ import { pipelineFindings, firstRefusal, PIPELINE_READ_KEYS } from "../../lint/o
|
|
|
31
31
|
export interface CollectStep {
|
|
32
32
|
/** The step's name; a later step reads its outputs as `{{ collect.<name>.<output> }}`. */
|
|
33
33
|
name: string;
|
|
34
|
-
/** The marketplace node it runs. */
|
|
34
|
+
/** The marketplace node it runs, or `InputTrigger` as the first step: the pipeline's door (README, `collect`). */
|
|
35
35
|
node: string;
|
|
36
36
|
/** The node's config, with `{{ config.* }}` and `{{ collect.* }}` references. */
|
|
37
37
|
config: Record<string, unknown>;
|
|
@@ -67,6 +67,12 @@ export interface MakeEntry {
|
|
|
67
67
|
when?: string;
|
|
68
68
|
/** How many of this object one source makes (owner, 2026-09-13): one, or many. The read asks by it; a part's entry says none. */
|
|
69
69
|
cardinality?: "one" | "many";
|
|
70
|
+
/**
|
|
71
|
+
* WRITTEN FROM EVERY SECTION (owner, 2026-10-05): `per: section` writes this object after the
|
|
72
|
+
* document is cut, one read per stored piece of each named section, instead of in the
|
|
73
|
+
* document's first read, which sees only its first two pages and its headings.
|
|
74
|
+
*/
|
|
75
|
+
per?: "section";
|
|
70
76
|
sourceUrl?: string;
|
|
71
77
|
sourceId?: string;
|
|
72
78
|
/** The item's content, each value a reference over `item`, `index`, `config`, `collect`. */
|
|
@@ -148,6 +154,7 @@ function readFolder(dir: string, name: string, org: string | undefined): Pipelin
|
|
|
148
154
|
...(spec.any === true ? { any: list } : { each: list }),
|
|
149
155
|
...(spec.cardinality === "one" || spec.cardinality === "many" ? { cardinality: spec.cardinality as "one" | "many" } : {}),
|
|
150
156
|
...(whenOf(spec) ? { when: whenOf(spec) } : {}),
|
|
157
|
+
...(spec.per === "section" ? { per: "section" as const } : {}),
|
|
151
158
|
sourceUrl: typeof raw.sourceUrl === "string" ? raw.sourceUrl : "{{ item.address }}",
|
|
152
159
|
sourceId: typeof raw.sourceId === "string" ? raw.sourceId : "{{ item.universalId }}",
|
|
153
160
|
content: isRecord(raw.content) ? raw.content : {},
|
|
@@ -295,7 +295,7 @@ function collectFrom(root: string, project: string, designSystem: string): Colle
|
|
|
295
295
|
// package envelope their $refs and credential shapes resolve against. The name is the
|
|
296
296
|
// node's `type`, bare: a saved workflow stores that identity and nothing else.
|
|
297
297
|
//
|
|
298
|
-
// Until 2026-09-05
|
|
298
|
+
// Until 2026-09-05 the collect never looked at nodes at all, so shipping an org sent a
|
|
299
299
|
// developer's components and skills and silently left every node behind. Until
|
|
300
300
|
// 2026-09-08 it looked at a `nodes/` folder beside design/ while every org already
|
|
301
301
|
// kept its nodes INSIDE its project, so the same nodes were left behind again. Org
|
|
@@ -461,8 +461,8 @@ function lintFile(file) {
|
|
|
461
461
|
// TEMPLATE manifest = the envelope. Requires binding + a resolvable root.
|
|
462
462
|
if (!json.name) report("warn", file, `app manifest missing "name": the display name (docs.unoverse.ai/design/apps)`);
|
|
463
463
|
checkDiscoveryMeta(report, file, json, "app", "docs.unoverse.ai/design/apps");
|
|
464
|
-
if (!(json.binding && json.binding.workflow))
|
|
465
|
-
report("warn", file, `template manifest has no binding.workflow. The app owns its workflow binding (docs.unoverse.ai/design/apps)`);
|
|
464
|
+
if (!(json.binding && (json.binding.workflow || json.binding.pipeline)))
|
|
465
|
+
report("warn", file, `template manifest has no binding.workflow or binding.pipeline. The app owns its workflow binding (docs.unoverse.ai/design/apps)`);
|
|
466
466
|
// Two valid roots (definitions.ts:229): the STANDARD manifest-only form (root =
|
|
467
467
|
// layouts/<layout>), OR a `<name>` envelope OVERRIDE (its own root). Only the
|
|
468
468
|
// manifest-only form must resolve a layout; an envelope-form template supplies its own.
|
|
@@ -23,7 +23,7 @@ export const KEBAB = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
|
23
23
|
const REF_OR_EXPR = /[{}]|^\s*return\b/;
|
|
24
24
|
|
|
25
25
|
/** The twelve elements of the vocabulary (docs/unoverse/data/objects/elements/): what an object IS. */
|
|
26
|
-
export const ELEMENTS = new Set(["purpose", "story", "content", "capability", "process", "asset", "task", "journey", "channel", "organisation", "people", "product", "brand", "document"]);
|
|
26
|
+
export const ELEMENTS = new Set(["purpose", "story", "content", "capability", "process", "asset", "task", "journey", "channel", "organisation", "people", "product", "brand", "document", "observation"]);
|
|
27
27
|
|
|
28
28
|
/** The face every object and pipeline carries, as a node's node.yaml does. */
|
|
29
29
|
export const FACE_KEYS = ["title", "description", "whenToUse", "category", "version"];
|
|
@@ -106,6 +106,20 @@ export function envelopeFindings(name, envelope, file = `${name}.yaml`) {
|
|
|
106
106
|
for (const k of Object.keys(l)) if (!["type", "between", "order", "when", "alike"].includes(k)) say(`object ${name}: link ${ln}: "${k}" is not a word here`);
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
// HOW A NAME IS MATCHED TO ITS ROWS (entity-resolution.md, "How an org declares it", 2026-10-05).
|
|
110
|
+
if (envelope.matching != null) {
|
|
111
|
+
const m = envelope.matching;
|
|
112
|
+
const fields = (v) => Array.isArray(v) && v.length > 0 && v.every((f) => typeof f === "string" && f);
|
|
113
|
+
if (!isRecord(m)) refuse(`object ${name}: matching is a record: ids, names, remember, evidence, ruleOut, judge, createFrom`);
|
|
114
|
+
else {
|
|
115
|
+
if (!fields(m.names)) refuse(`object ${name}: matching: names lists the fields that hold this object's names`);
|
|
116
|
+
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
|
+
if (m.createFrom != null && !fields(m.createFrom)) refuse(`object ${name}: matching: createFrom is a list of line names (about)`);
|
|
118
|
+
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
|
+
if (m.judge != null && (typeof m.judge !== "string" || !/^[A-Z][A-Za-z0-9]*$/.test(m.judge))) refuse(`object ${name}: matching: judge names a node by its type (TypeSafeChoice)`);
|
|
120
|
+
for (const k of Object.keys(m)) if (!["ids", "names", "remember", "evidence", "ruleOut", "judge", "createFrom"].includes(k)) say(`object ${name}: matching: "${k}" is not a word here`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
109
123
|
return out;
|
|
110
124
|
}
|
|
111
125
|
|
|
@@ -251,6 +265,11 @@ export function rowKeyFindings(name, envelope, props, file = `${name}.yaml`) {
|
|
|
251
265
|
if (!Array.isArray(list)) continue;
|
|
252
266
|
for (const f of list.flat()) if (typeof f === "string" && !known.has(f) && !known.has(camel(f))) out.push({ file, msg: `object ${name}: ${key} names "${f}", which is not a prop of this object or its base. A row key is a field (README 9a)` });
|
|
253
267
|
}
|
|
268
|
+
const m = isRecord(envelope.matching) ? envelope.matching : null;
|
|
269
|
+
if (m) {
|
|
270
|
+
for (const k of ["ids", "names", "evidence", "ruleOut"]) for (const f of Array.isArray(m[k]) ? m[k] : []) if (typeof f === "string" && !known.has(f)) out.push({ file, msg: `object ${name}: matching: ${k} names "${f}", which is not a prop of this object or its base (entity-resolution.md)` });
|
|
271
|
+
if (typeof m.remember === "string" && props[m.remember] && props[m.remember].type !== "array") out.push({ file, msg: `object ${name}: matching: remember names "${m.remember}", which is not a list; a confirmed name is added to a list (entity-resolution.md)` });
|
|
272
|
+
}
|
|
254
273
|
return out;
|
|
255
274
|
}
|
|
256
275
|
|
|
@@ -285,7 +304,7 @@ export const PIPELINE_READ_KEYS = new Set(["each", "sourceUrl", "sourceId", "con
|
|
|
285
304
|
const DROPPED_MAKE_KEYS = ["partsOf", "by", "under", "when", "from", "any", "index"];
|
|
286
305
|
|
|
287
306
|
/** What an object may say: under `each`, how many and what it holds; made once per pack, how many and from which rows. */
|
|
288
|
-
const ITEM_OBJECT_KEYS = new Set(["cardinality", "any", "parts", "where"]);
|
|
307
|
+
const ITEM_OBJECT_KEYS = new Set(["cardinality", "any", "parts", "where", "per"]);
|
|
289
308
|
const FIELD = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
290
309
|
|
|
291
310
|
/**
|
|
@@ -315,6 +334,7 @@ export function pipelineFindings(name, file, fileName = "pipeline.yaml") {
|
|
|
315
334
|
return;
|
|
316
335
|
}
|
|
317
336
|
if (stepNames.has(s.name)) refuse(`pipeline ${name}: two collect steps named "${s.name}"`);
|
|
337
|
+
if (s.node === "InputTrigger" && i > 0) refuse(`pipeline ${name}: collect step "${s.name}" is an Input Trigger, which only opens a pipeline: make it the first step (docs/unoverse/data/pipelines/README.md, collect)`);
|
|
318
338
|
// A step reads only what came before it.
|
|
319
339
|
for (const ref of collectRefs(s.config)) if (!stepNames.has(ref)) say(`pipeline ${name}: collect step "${s.name}" reads collect.${ref}, which is not an earlier step`);
|
|
320
340
|
stepNames.add(s.name);
|
|
@@ -361,7 +381,8 @@ export function pipelineFindings(name, file, fileName = "pipeline.yaml") {
|
|
|
361
381
|
}
|
|
362
382
|
if (made.includes(o)) refuse(`pipeline ${name}: ${at}.${o} is made above already; one entry makes an object`);
|
|
363
383
|
const allowed = ITEM_OBJECT_KEYS;
|
|
364
|
-
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 and
|
|
384
|
+
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 and per (${see})`);
|
|
385
|
+
if ("per" in spec && spec.per !== "section") refuse(`pipeline ${name}: ${at}.${o}: per says section, the only unit an object is written per (${see})`);
|
|
365
386
|
// HOW MANY IS SAID by every object the model writes (owner, 2026-09-13).
|
|
366
387
|
if (spec.cardinality !== "one" && spec.cardinality !== "many") say(`pipeline ${name}: ${at}.${o} says cardinality: one or many, how many of it one row makes (${see})`);
|
|
367
388
|
if ("any" in spec && spec.any !== true) refuse(`pipeline ${name}: ${at}.${o}: any: true is its only value, the item MAY be one`);
|
|
@@ -419,7 +440,8 @@ export function pipelineFolderFindings(name, { file, config, test }, { nodeTypes
|
|
|
419
440
|
const out = [];
|
|
420
441
|
if (!isRecord(file)) return out;
|
|
421
442
|
const steps = Array.isArray(file.collect) ? file.collect.filter(isRecord) : [];
|
|
422
|
-
|
|
443
|
+
// The Input Trigger is the engine's own node, not a marketplace one: the pipeline's door (README, collect).
|
|
444
|
+
if (nodeTypes) for (const s of steps) if (typeof s.node === "string" && s.node !== "InputTrigger" && !nodeTypes.has(s.node)) out.push({ file: "pipeline.yaml", msg: `pipeline ${name}: collect step "${s.name}" names node "${s.node}", which neither the marketplace nor this org holds. A step runs a node the org has` });
|
|
423
445
|
if (objectNames && isRecord(file.make)) for (const object of Object.keys(file.make)) if (!objectNames.has(object)) out.push({ file: "pipeline.yaml", msg: `pipeline ${name}: make.${object} names an object neither the marketplace nor this org holds. What a pipeline makes is an object folder: design/<tier>/objects/${object}/` });
|
|
424
446
|
const props = isRecord(config?.configSchema?.properties) ? new Set(Object.keys(config.configSchema.properties)) : null;
|
|
425
447
|
const refs = configRefs(file);
|
|
@@ -45,7 +45,7 @@ const MEMORY_SERVICE_URL = process.env.MEMORY_SERVICE_URL || "http://localhost:4
|
|
|
45
45
|
export const USER_MEMORY_METHODS: Record<string, any> = {
|
|
46
46
|
queryMemory: {
|
|
47
47
|
description:
|
|
48
|
-
"
|
|
48
|
+
"Memory about this person: who they are, what they have told you, their preferences, and anything they started and did not finish. Use it only to know who is asking or to pick up their unfinished work. It holds no records, figures or actions, and it never finds a tool. When the person asks you to do something, or asks about their own records, search for it instead. Returns claims with certainty scores; weight stronger evidence higher. (A summary of who the person is was already provided in your context; query only for depth beyond it.)",
|
|
49
49
|
input: {
|
|
50
50
|
type: "object",
|
|
51
51
|
additionalProperties: false,
|
|
@@ -133,7 +133,7 @@ export const AGENT_MEMORY_METHODS: Record<string, any> = {
|
|
|
133
133
|
};
|
|
134
134
|
|
|
135
135
|
export const USER_MEMORY_INSTRUCTIONS =
|
|
136
|
-
"User memory is
|
|
136
|
+
"User memory is about the person: who they are, what they have told you, and what they left unfinished. A summary is already in your context; do not re-fetch it. queryMemory goes deeper on a topic about the person. Memory is never how you get something done: for any task or question about their own records, use search to find the tool, then answer for this person.";
|
|
137
137
|
|
|
138
138
|
export const AGENT_MEMORY_INSTRUCTIONS =
|
|
139
139
|
"You have persistent goal memory that survives restarts and context resets. On wake: getGoalContext before doing work — it shows the active goal, its ledger (attempts, whether it is met, the criteria still open from the judge's last verdict), AND your recent goals with their progress (done/total). If an earlier goal is unfinished or was closed too soon, resumeGoal reopens it so you continue from where you left off instead of starting over. While working: record decisions, failures, environment facts, and progress with writeNote, and keep updateGoalState accurate after each meaningful step (mark steps '[done] …' as you finish them — don't leave the plan frozen on step 1). On completion: REVIEW first — verify the output meets the locked acceptance criteria, mark the remaining plan steps done and set a closing currentStep via updateGoalState, then archiveGoal with a specific summary. The journal is for future-you — write what re-hydration needs, nothing else.";
|
package/vendor/sdk/appEngine.ts
CHANGED
|
@@ -227,6 +227,7 @@ export function mountAppEngine(el: HTMLElement, first: AppConfig): AppEngine {
|
|
|
227
227
|
// A PAGE TEMPLATE'S TASK, called at its own workflow's trigger with the user's token.
|
|
228
228
|
// `metadata.task` names the task its Task Runner loads; the submit carries the answers.
|
|
229
229
|
const startTask = async (b: Binding, task: string, submit?: Record<string, unknown>) => {
|
|
230
|
+
if (b.pipeline) return startPipeline(b.pipeline, submit ?? {});
|
|
230
231
|
if (!b.workflow) return;
|
|
231
232
|
inTask = true;
|
|
232
233
|
syncConnection();
|
|
@@ -248,6 +249,27 @@ export function mountAppEngine(el: HTMLElement, first: AppConfig): AppEngine {
|
|
|
248
249
|
}).catch((e) => console.error("[unoverse] task start failed", e));
|
|
249
250
|
};
|
|
250
251
|
|
|
252
|
+
// A TEMPLATE BOUND TO A PIPELINE (docs/unoverse/data/pipelines/README.md, collect): the answers go
|
|
253
|
+
// to the pipeline's Input Trigger, called as a workflow is, `/api/pipelines/<org>/<name>/execute`,
|
|
254
|
+
// the org the app's own. The server shows the run live in this conversation, as it shows a
|
|
255
|
+
// workflow task (the agent-card, its steps from the run's traces), so nothing here follows it;
|
|
256
|
+
// the receipt is written into template state as `run`.
|
|
257
|
+
const startPipeline = async (name: string, answers: Record<string, unknown>) => {
|
|
258
|
+
const org = appId.includes("/") ? appId.split("/")[0] : "";
|
|
259
|
+
const token = cfg.getAccessToken ? await cfg.getAccessToken() : null;
|
|
260
|
+
try {
|
|
261
|
+
const res = await fetch(`${cfg.apiUrl ?? cfg.serverUrl}/api/pipelines/${encodeURIComponent(org)}/${encodeURIComponent(name)}/execute`, {
|
|
262
|
+
method: "POST",
|
|
263
|
+
headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
|
|
264
|
+
body: JSON.stringify({ input: answers, conversationId: cfg.conversationId, chatId: cfg.conversationId }),
|
|
265
|
+
});
|
|
266
|
+
store.mergeAppState({ run: await res.json().catch(() => ({ status: res.ok ? "STARTED" : "FAILED" })) });
|
|
267
|
+
} catch (e) {
|
|
268
|
+
console.error("[unoverse] pipeline run failed", e);
|
|
269
|
+
store.mergeAppState({ run: { status: "FAILED", error: String((e as Error)?.message ?? e) } });
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
251
273
|
const onAction: ActionHandler = (action, data, meta) => {
|
|
252
274
|
const t = typeof action === "string" ? action : (action as { type?: string })?.type;
|
|
253
275
|
const d = (data ?? {}) as Record<string, unknown>;
|
|
@@ -282,10 +282,15 @@ export function mountComponent(options: ComponentOptions): ComponentMount {
|
|
|
282
282
|
tree.destroy();
|
|
283
283
|
show([message(doc, text)]);
|
|
284
284
|
};
|
|
285
|
-
//
|
|
285
|
+
// An error carries NO styling: the SDK owns zero styles. Loading shows nothing: a comment
|
|
286
|
+
// holds the component's place until the drawing arrives, so a slide never flashes a line.
|
|
287
|
+
const hold = () => {
|
|
288
|
+
tree.destroy();
|
|
289
|
+
show([doc.createComment(`loading ${defUri ?? "component"}`)]);
|
|
290
|
+
};
|
|
286
291
|
if (error) return say(`Unoverse error: ${error}`);
|
|
287
|
-
if (o.store && !o.store.getType(o.chatId ?? "", o.nodeId ?? "")) return
|
|
288
|
-
if (!def || !theme) return
|
|
292
|
+
if (o.store && !o.store.getType(o.chatId ?? "", o.nodeId ?? "")) return hold();
|
|
293
|
+
if (!def || !theme) return hold();
|
|
289
294
|
// Defaults beneath; the app's state, then the definition's own state, then the
|
|
290
295
|
// instance's values above; a value the component wrote itself on top (static source).
|
|
291
296
|
const merged = o.store
|
|
@@ -534,8 +534,12 @@ export function inputView(ctx: RenderCtx): Prim {
|
|
|
534
534
|
const max = maxLength();
|
|
535
535
|
// Focus moves BEFORE the store round-trip, so a fast typist's next key lands next door.
|
|
536
536
|
if (advanceOnFill() && max != null && t.length >= max) stepFocus(1);
|
|
537
|
-
if (field() != null)
|
|
538
|
-
|
|
537
|
+
if (field() != null) {
|
|
538
|
+
ctx.onAction?.("input", { text: t, field: field() });
|
|
539
|
+
// The store tells the page on the next frame; until then the field shows what was typed,
|
|
540
|
+
// not the old value, so a fast typist's next key lands after it, not in an emptied box.
|
|
541
|
+
data = { ...data, [field()!]: t };
|
|
542
|
+
} else local = t;
|
|
539
543
|
show();
|
|
540
544
|
};
|
|
541
545
|
const submit = () => {
|
|
@@ -67,7 +67,7 @@ const avatars = new Set<HTMLElement>();
|
|
|
67
67
|
const CALL_PHASES = ["listening", "speaking"];
|
|
68
68
|
|
|
69
69
|
/** The workflow a template names in its manifest (UNOVERSE_HOST_PAGE.md rule 4). */
|
|
70
|
-
export type Binding = { workflow?: string; trigger?: string };
|
|
70
|
+
export type Binding = { workflow?: string; trigger?: string; pipeline?: string };
|
|
71
71
|
|
|
72
72
|
/** THE TASK IN VIEW: the marked template, not an avatar, spanning the middle of the screen. */
|
|
73
73
|
function taskInView(doc: Document): HTMLElement | null {
|
|
@@ -152,6 +152,7 @@ function mountPageTemplate(o: PageTemplateOptions): { refresh(): void; destroy()
|
|
|
152
152
|
// `props`, keyed by part, merge into those parts. THE SAME TEMPLATE, STREAMED, hydrates this
|
|
153
153
|
// copy part by part; a copy still on its first step moves to the next as its values land.
|
|
154
154
|
let lastProps = "";
|
|
155
|
+
let lastStep: string | undefined;
|
|
155
156
|
let lastStreamed = "";
|
|
156
157
|
const lastPart = new Map<string, string>();
|
|
157
158
|
const fillParts = (byPart: Record<string, unknown>) => {
|
|
@@ -176,7 +177,12 @@ function mountPageTemplate(o: PageTemplateOptions): { refresh(): void; destroy()
|
|
|
176
177
|
fillParts(task.props);
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
|
-
|
|
180
|
+
// A NEW STEP MOVES IT, once. A step the person took since (Enter) stands until the task
|
|
181
|
+
// publishes another: the latest move wins, wherever it was made.
|
|
182
|
+
if (task?.step !== lastStep) {
|
|
183
|
+
lastStep = task?.step;
|
|
184
|
+
moveTo(task?.step);
|
|
185
|
+
}
|
|
180
186
|
for (const r of appStore.getResponses()) {
|
|
181
187
|
for (const pointer of r.components) {
|
|
182
188
|
const { chatId, nodeId } = appStore.split(pointer);
|
|
@@ -288,7 +294,7 @@ function mountPageTemplate(o: PageTemplateOptions): { refresh(): void; destroy()
|
|
|
288
294
|
.then((raw) => {
|
|
289
295
|
if (!alive || !raw) return;
|
|
290
296
|
const d = raw as UnoverseDefinition & { defaultLayout?: string; layouts?: Record<string, unknown>; sections?: Record<string, Section[]>; binding?: Binding; stateTree?: StateTree };
|
|
291
|
-
if (d.binding?.workflow) o.onBinding(uri, d.binding);
|
|
297
|
+
if (d.binding?.workflow || d.binding?.pipeline) o.onBinding(uri, d.binding);
|
|
292
298
|
def = d;
|
|
293
299
|
const base = d.defaultLayout ?? (d.layouts ? Object.keys(d.layouts)[0] : undefined);
|
|
294
300
|
parts = [];
|
|
@@ -299,6 +305,8 @@ function mountPageTemplate(o: PageTemplateOptions): { refresh(): void; destroy()
|
|
|
299
305
|
parts.push(nodeId);
|
|
300
306
|
store.apply({ type: "COMPONENT_INIT", chatId: CHAT, nodeId, component: { type: ref, props: sec.seeds?.[ref] ?? {} } });
|
|
301
307
|
if (sec.face) store.mergeComponentState(CHAT, nodeId, { state: sec.face });
|
|
308
|
+
// EVERY PART READ NOW, not when its face first shows: a step onto a slide draws at once.
|
|
309
|
+
client.readDefinition(`unoverse://components/${ref}`).catch(() => {});
|
|
302
310
|
});
|
|
303
311
|
}
|
|
304
312
|
const ref = uri.replace(/^unoverse:\/\/templates\//, "");
|
|
@@ -366,7 +374,7 @@ export function mountPageInterfaces(o: PageInterfacesOptions): { destroy(): void
|
|
|
366
374
|
if (action === "submit" || action === "cancel") {
|
|
367
375
|
o.toServer(action, data, meta);
|
|
368
376
|
const state = (meta?.state ?? data) as Record<string, unknown> | undefined;
|
|
369
|
-
if (action === "submit" && bindings[uri]?.workflow && state && typeof state === "object") o.startTask(bindings[uri], uri, state);
|
|
377
|
+
if (action === "submit" && (bindings[uri]?.workflow || bindings[uri]?.pipeline) && state && typeof state === "object") o.startTask(bindings[uri], uri, state);
|
|
370
378
|
} else if (action === "startCall") {
|
|
371
379
|
const t = target();
|
|
372
380
|
if (!t || started.has(t)) return;
|