@unoverse-platform/studio 0.1.111 → 0.1.113

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 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. Until your universe runs a release with the Git sync,
58
- `unoverse deploy studio` still publishes to your own universe over its API. Lint runs
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 (written
368
- * by `unoverse deploy studio`, committed with the repo) names where this
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unoverse-platform/studio",
3
- "version": "0.1.111",
3
+ "version": "0.1.113",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {
@@ -96,6 +96,26 @@ 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
+ }
118
+
99
119
  /**
100
120
  * A COLUMN OF A DATASET (dataset.md, "the declaration is the schema"). The same words a prop
101
121
  * uses, so an author learns one grammar: a name, a type, whether it must be there, and whether
@@ -182,6 +202,8 @@ export interface ObjectDefinition {
182
202
  meta?: "open";
183
203
  /** The lines it draws between its own rows, by name (README 7; relationships-and-labels.md). */
184
204
  links?: Record<string, ObjectLink>;
205
+ /** How a name is matched to its rows (entity-resolution.md); absent, a line to it compares one field with one field. */
206
+ matching?: ObjectMatching;
185
207
  /** THE TABLE THIS OBJECT OWNS (docs/unoverse/data/objects/dataset.md, PROPOSED 2026-09-18),
186
208
  * from its folder's table.yaml: an object that holds many small facts read together rather
187
209
  * than one row read alone. Absent on every ordinary object, which stays a row. */
@@ -325,6 +347,11 @@ function readFolder(dir: string, name: string, org: string | undefined, seen: st
325
347
  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
348
  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
349
  }
350
+ // ITS MATCHING names only its own fields, for the same reason.
351
+ const matching = (envelope.matching ?? parent?.matching) as ObjectMatching | undefined;
352
+ for (const f of [...(matching?.ids ?? []), ...(matching?.names ?? []), ...(matching?.evidence ?? []), ...(matching?.ruleOut ?? [])]) {
353
+ 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})`);
354
+ }
328
355
  return {
329
356
  name,
330
357
  ...(org ? { org } : {}),
@@ -338,6 +365,7 @@ function readFolder(dir: string, name: string, org: string | undefined, seen: st
338
365
  ...(typeof envelope.element === "string" && envelope.element ? { element: envelope.element } : parent?.element ? { element: parent.element } : {}),
339
366
  ...(envelope.meta ? { meta: envelope.meta } : parent?.meta ? { meta: parent.meta } : {}),
340
367
  ...(links && Object.keys(links).length ? { links } : {}),
368
+ ...(envelope.matching ? { matching: envelope.matching as ObjectMatching } : parent?.matching ? { matching: parent.matching } : {}),
341
369
  ...(dataset ? { dataset } : {}),
342
370
  props,
343
371
  ownProps,
@@ -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 deploy never looked at nodes at all, so `deploy studio` shipped a
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
@@ -106,6 +106,19 @@ 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`);
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.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`);
118
+ 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)`);
119
+ for (const k of Object.keys(m)) if (!["ids", "names", "remember", "evidence", "ruleOut", "judge"].includes(k)) say(`object ${name}: matching: "${k}" is not a word here`);
120
+ }
121
+ }
109
122
  return out;
110
123
  }
111
124
 
@@ -251,6 +264,11 @@ export function rowKeyFindings(name, envelope, props, file = `${name}.yaml`) {
251
264
  if (!Array.isArray(list)) continue;
252
265
  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
266
  }
267
+ const m = isRecord(envelope.matching) ? envelope.matching : null;
268
+ if (m) {
269
+ 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)` });
270
+ 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)` });
271
+ }
254
272
  return out;
255
273
  }
256
274
 
@@ -285,7 +303,7 @@ export const PIPELINE_READ_KEYS = new Set(["each", "sourceUrl", "sourceId", "con
285
303
  const DROPPED_MAKE_KEYS = ["partsOf", "by", "under", "when", "from", "any", "index"];
286
304
 
287
305
  /** 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"]);
306
+ const ITEM_OBJECT_KEYS = new Set(["cardinality", "any", "parts", "where", "per"]);
289
307
  const FIELD = /^[A-Za-z_][A-Za-z0-9_]*$/;
290
308
 
291
309
  /**
@@ -361,7 +379,8 @@ export function pipelineFindings(name, file, fileName = "pipeline.yaml") {
361
379
  }
362
380
  if (made.includes(o)) refuse(`pipeline ${name}: ${at}.${o} is made above already; one entry makes an object`);
363
381
  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 where (${see})`);
382
+ 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})`);
383
+ 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
384
  // HOW MANY IS SAID by every object the model writes (owner, 2026-09-13).
366
385
  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
386
  if ("any" in spec && spec.any !== true) refuse(`pipeline ${name}: ${at}.${o}: any: true is its only value, the item MAY be one`);
@@ -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
- "Search the user's remembered facts by topic (semantic + keyword). Use mid-conversation when a specific topic needs prior context — budget, preferences, past decisions about X, or where an earlier plan got to. Returns claims with certainty scores — weight stronger evidence higher. This is evidence about the user specifically, not a knowledge-base search. (A summary of who the user is was already provided in your context — query only for depth beyond it.)",
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 available. A background summary of who this user is (identity + current state) was injected into your context at start — do not re-fetch it. queryMemory(topic) searches deeper when a specific topic needs prior context.";
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.";