unoverse 0.1.199 → 0.1.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/lib/skills.mjs +32 -26
  2. package/operator/.env.example +1 -1
  3. package/operator/docker-compose.yml +1 -1
  4. package/operator/infra/aws/main.tf +1 -1
  5. package/operator/infra/digitalocean/main.tf +1 -1
  6. package/operator/lib/db-verify.sh +13 -10
  7. package/operator/lib/setup.sh +1 -1
  8. package/operator/operator.sh +1 -1
  9. package/package.json +1 -1
  10. package/vendor/base/items/collect.js +6 -2
  11. package/vendor/base/items/publish.js +1 -1
  12. package/vendor/base/lint/design/vocabulary.mjs +1 -1
  13. package/vendor/base/lint/nodes/_schema/_defs.schema.json +2 -2
  14. package/vendor/base/lint/nodes/_schema/api.schema.json +2 -2
  15. package/vendor/base/lint/nodes/_schema/audio.schema.json +1 -1
  16. package/vendor/base/lint/nodes/_schema/config.schema.json +5 -5
  17. package/vendor/base/lint/nodes/_schema/credential.schema.json +3 -3
  18. package/vendor/base/lint/nodes/_schema/events.schema.json +6 -0
  19. package/vendor/base/lint/nodes/_schema/interface.schema.json +2 -2
  20. package/vendor/base/lint/nodes/_schema/narrate.schema.json +6 -0
  21. package/vendor/base/lint/nodes/_schema/node.schema.json +15 -7
  22. package/vendor/base/lint/nodes/_schema/package.schema.json +5 -5
  23. package/vendor/base/lint/nodes/_schema/run.schema.json +6 -0
  24. package/vendor/base/lint/nodes/_schema/service.schema.json +6 -0
  25. package/vendor/base/lint/nodes/_schema/test.schema.json +8 -8
  26. package/vendor/base/lint/nodes/_schema/toolExchange.schema.json +6 -0
  27. package/vendor/base/lint/nodes/index.mjs +1 -1
  28. package/vendor/base/lint/nodes/schema.mjs +7 -7
  29. package/vendor/base/manifests/source.js +1 -1
package/lib/skills.mjs CHANGED
@@ -1,10 +1,12 @@
1
1
  // Install the Claude Code authoring skills for this developer, FROM THE DOCS SITE.
2
2
  //
3
- // The skills are pages of the documentation (packages/docs/skills/<skill>/...), published
4
- // with every other page and listed in the site's llms.txt (owner ruling 2026-09-05: the
5
- // docs site is the one public origin, and it already serves every page as raw markdown).
6
- // The installer reads llms.txt, takes every page under /skills/, and writes each one to
7
- // disk. There is no GitHub mirror and no tarball any more.
3
+ // The skills are files under packages/docs/skills/<skill>/, served by the docs site at the
4
+ // same path. They are NOT in the site's navigation (owner ruling 2026-09-05: a skill is
5
+ // for the agent to read, not a page for a person to browse), so the installer does not
6
+ // read llms.txt. It reads `skills/skills.json` for the list of skills, then each skill's
7
+ // SKILL.md, and follows every `references/<name>.md` the front page names. A reference the
8
+ // front page does not name never installs, and a test in packages/docs keeps the two in
9
+ // step.
8
10
  //
9
11
  // PER DEVELOPER, NOT PER PROJECT (2026-08-21). These install to ~/.claude/skills, so
10
12
  // Claude Code picks them up in every folder the developer opens and nothing lands
@@ -20,18 +22,14 @@ import { tmpdir, homedir } from "node:os";
20
22
  const DOCS = "https://docs.unoverse.ai";
21
23
 
22
24
  /**
23
- * The skill files a site's llms.txt names: `{ skill, rel, url }` per page under /skills/,
24
- * `rel` being the path inside the skill folder. A skill's front page is SKILL.md on disk
25
- * whatever case the site serves it in, because Claude Code looks for exactly that name.
26
- * Pure, so it can be checked without a network.
25
+ * The reference files a skill's front page names, as `references/<name>.md`, in order of
26
+ * first mention and without repeats. Pure, so it can be checked without a network.
27
27
  */
28
- export function skillFilesFromLlms(text, origin = DOCS) {
28
+ export function referencesNamedBy(skillMd) {
29
29
  const out = [];
30
- const re = new RegExp(`\\((${origin.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}/skills/([^/\\s)]+)/([^\\s)]+\\.md))\\)`, "g");
31
- for (const m of text.matchAll(re)) {
32
- const [, url, skill, path] = m;
33
- const rel = basename(path).toLowerCase() === "skill.md" ? join(dirname(path), "SKILL.md") : path;
34
- out.push({ skill, rel, url });
30
+ for (const m of skillMd.matchAll(/references\/([a-z0-9-]+)\.md/g)) {
31
+ const rel = `references/${m[1]}.md`;
32
+ if (!out.includes(rel)) out.push(rel);
35
33
  }
36
34
  return out;
37
35
  }
@@ -90,30 +88,38 @@ const fresh = (url) => `${url}?t=${Date.now()}`;
90
88
 
91
89
  export async function installSkills() {
92
90
  const target = join(homedir(), ".claude", "skills");
93
- let files;
91
+ let skills;
94
92
  try {
95
- const res = await fetch(fresh(`${DOCS}/llms.txt`));
93
+ const res = await fetch(fresh(`${DOCS}/skills/skills.json`));
96
94
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
97
- files = skillFilesFromLlms(await res.text());
95
+ skills = (await res.json()).skills ?? [];
98
96
  } catch {
99
97
  console.log(` Skills install skipped (${DOCS} unreachable). They install on the next update.`);
100
98
  return;
101
99
  }
102
- const skills = [...new Set(files.map((f) => f.skill))];
103
100
  if (!skills.length) {
104
- console.log(` Skills install skipped: ${DOCS}/llms.txt lists no skills yet.`);
101
+ console.log(` Skills install skipped: ${DOCS}/skills/skills.json lists no skills.`);
105
102
  return;
106
103
  }
107
104
  // Fetched whole into a scratch folder first, so a download that dies halfway leaves the
108
105
  // installed copy untouched rather than half-replaced.
109
106
  const tmp = mkdtempSync(join(tmpdir(), "unoverse-skills-"));
110
107
  try {
111
- for (const f of files) {
112
- const res = await fetch(fresh(f.url));
113
- if (!res.ok) throw new Error(`${f.url}: HTTP ${res.status}`);
114
- const p = join(tmp, f.skill, f.rel);
115
- mkdirSync(dirname(p), { recursive: true });
116
- writeFileSync(p, skillFileText(f.skill, f.rel, await res.text()));
108
+ for (const skill of skills) {
109
+ const front = await fetch(fresh(`${DOCS}/skills/${skill}/SKILL.md`));
110
+ if (!front.ok) throw new Error(`${skill}/SKILL.md: HTTP ${front.status}`);
111
+ const frontText = await front.text();
112
+ const files = [["SKILL.md", frontText]];
113
+ for (const rel of referencesNamedBy(frontText)) {
114
+ const res = await fetch(fresh(`${DOCS}/skills/${skill}/${rel}`));
115
+ if (!res.ok) throw new Error(`${skill}/${rel}: HTTP ${res.status}`);
116
+ files.push([rel, await res.text()]);
117
+ }
118
+ for (const [rel, text] of files) {
119
+ const p = join(tmp, skill, rel);
120
+ mkdirSync(dirname(p), { recursive: true });
121
+ writeFileSync(p, skillFileText(skill, rel, text));
122
+ }
117
123
  }
118
124
  mkdirSync(target, { recursive: true });
119
125
  // REPLACED WHOLE, ours only. Each skill folder the site carries is removed and
@@ -42,7 +42,7 @@ DOCR_TOKEN=
42
42
  # Page intelligence (promote-page extraction)
43
43
  HYPERBROWSER_API_KEY=
44
44
 
45
- # Marketplace catalogue to install from (docs/architecture/MARKETPLACE.md).
45
+ # Marketplace catalogue to install from (docs/architecture/authoring/MARKETPLACE.md).
46
46
  # Empty = local items only. No default: a URL is never hardcoded.
47
47
  UNOVERSE_MARKETPLACE_URL=
48
48
 
@@ -372,7 +372,7 @@ services:
372
372
  restart: unless-stopped
373
373
 
374
374
  # unoverse-runtime REMOVED 2026-07-28: WIP, excluded from the platform
375
- # (docs/architecture/INFRASTRUCTURE.md). Returns when the user declares it ready.
375
+ # (docs/architecture/deployment/INFRASTRUCTURE.md). Returns when the user declares it ready.
376
376
 
377
377
  # ===========================================================================
378
378
  # OBSERVABILITY — log viewer (runs by default)
@@ -1,4 +1,4 @@
1
- # The Universe — AWS POC (docs/architecture/AWS_DEPLOYMENT.md)
1
+ # The Universe — AWS POC (docs/architecture/deployment/AWS_DEPLOYMENT.md)
2
2
  #
3
3
  # One VM + managed Postgres/Redis + Cognito + a scoped Bedrock IAM user.
4
4
  # Deliberately flat and minimal: this is the POC tier. Terraform provisions,
@@ -1,4 +1,4 @@
1
- # The Universe — DigitalOcean ground (docs/architecture/INFRASTRUCTURE.md)
1
+ # The Universe — DigitalOcean ground (docs/architecture/deployment/INFRASTRUCTURE.md)
2
2
  #
3
3
  # Same five-input contract as infra/aws, DO implementation: Droplet + Managed
4
4
  # Postgres (fronted by its built-in PgBouncer, per the Postgres law) + Managed
@@ -36,7 +36,7 @@ cmd_db_verify() {
36
36
  "id", "name", "description", "nodes", "edges", "active",
37
37
  "execution_mode", "test_inputs", "umap_settings", "viewport",
38
38
  "mcp_schema", "memory_config", "content_taxonomy", "created_at", "updated_at",
39
- "workflow_document", "layout_document"
39
+ "workflow_document", "layout_document", "org"
40
40
  ],
41
41
  workflow_executions: [
42
42
  "execution_id", "workflow_id", "status", "start_time", "end_time",
@@ -93,42 +93,42 @@ cmd_db_verify() {
93
93
  "needs", "source_url", "source_id", "umap_x", "umap_y", "umap_z",
94
94
  "umap_cluster_id", "color_hex", "needs_umap_update", "created_at",
95
95
  "updated_at", "workflow_id", "key_need", "metadata", "embedding_original",
96
- "cluster_distance"
96
+ "cluster_distance", "org"
97
97
  ],
98
98
  dictionary_clusters: [
99
99
  "cluster_id", "workflow_id", "parent_id", "depth", "name", "description",
100
100
  "name_embedding", "medoid_id", "terms", "member_count", "umap_x", "umap_y",
101
101
  "umap_z", "radius", "created_at", "updated_at",
102
102
  "region_id", "carried_overlap", "derived_from", "name_locked", "locked_at",
103
- "locked_by", "member_ids"
103
+ "locked_by", "member_ids", "org"
104
104
  ],
105
105
  user_region_events: [
106
106
  "id", "workflow_id", "user_id", "region_id", "region_name", "stage",
107
- "source", "evidence", "occurred_at"
107
+ "source", "evidence", "occurred_at", "org"
108
108
  ],
109
109
  dictionary_regions: [
110
110
  "region_id", "workflow_id", "depth", "name", "description", "name_locked",
111
111
  "locked_at", "locked_by", "stage", "stage_set_by", "needs_review",
112
112
  "skills", "skills_set_by", "skills_set_at",
113
- "derived_from", "first_seen", "last_seen", "closed_at"
113
+ "derived_from", "first_seen", "last_seen", "closed_at", "org"
114
114
  ],
115
115
  dictionary_content_chunks: [
116
116
  "chunk_id", "text", "source_url", "source_type", "metadata",
117
- "workflow_id", "created_at", "updated_at", "embedding_original"
117
+ "workflow_id", "created_at", "updated_at", "embedding_original", "org"
118
118
  ],
119
119
  dictionary_chunk_need_matches: [
120
120
  "chunk_id", "need_id", "similarity_score", "rank", "match_type",
121
- "created_at", "workflow_id"
121
+ "created_at", "workflow_id", "org"
122
122
  ],
123
123
  dictionary_ingestion_configs: [
124
124
  "id", "name", "description", "connector_type", "connector_config",
125
125
  "main_category", "workflow_id", "created_at", "updated_at",
126
- "last_run_at", "run_count"
126
+ "last_run_at", "run_count", "org"
127
127
  ],
128
128
  dictionary_ingestion_jobs: [
129
129
  "job_id", "workflow_id", "status", "connector_type", "category",
130
130
  "config", "extraction_config", "started_at", "completed_at",
131
- "progress", "error", "created_at"
131
+ "progress", "error", "created_at", "org"
132
132
  ],
133
133
  goals: [
134
134
  "goal_id", "user_id", "workflow_id", "status", "description",
@@ -141,13 +141,16 @@ cmd_db_verify() {
141
141
  goal_scratch: [
142
142
  "goal_id", "agent_id", "agent_name", "state", "updated_at"
143
143
  ],
144
+ orgs: [
145
+ "slug", "name", "settings", "website", "logo", "created_at", "updated_at"
146
+ ],
144
147
  knowledge_docs: [
145
148
  "id", "workflow_id", "title", "doc_type", "sections", "version",
146
149
  "links", "metadata", "created_at", "updated_at"
147
150
  ],
148
151
  content_sources: [
149
152
  "workflow_id", "connector", "source_key", "group_path", "label",
150
- "status", "last_ingested", "metadata", "created_at", "updated_at"
153
+ "status", "last_ingested", "metadata", "created_at", "updated_at", "org"
151
154
  ],
152
155
  security_attack_corpus: [
153
156
  "id", "category", "label", "attack_prompt", "expected_result",
@@ -343,7 +343,7 @@ OPENAI_API_KEY=${OPENAI_API_KEY}
343
343
  # back it up with the database. Kept on re-runs.
344
344
  CREDENTIAL_ENCRYPTION_KEY=${CREDENTIAL_ENCRYPTION_KEY}
345
345
  HYPERBROWSER_API_KEY=${HYPERBROWSER_API_KEY}
346
- # Marketplace catalogue to install from (docs/architecture/MARKETPLACE.md).
346
+ # Marketplace catalogue to install from (docs/architecture/authoring/MARKETPLACE.md).
347
347
  # Empty = local items only. No default: a URL is never hardcoded.
348
348
  UNOVERSE_MARKETPLACE_URL=
349
349
  DOMAIN=
@@ -223,7 +223,7 @@ case "${1:-}" in
223
223
  echo "" >&2
224
224
  echo " scripts/operator.sh publish" >&2
225
225
  echo "" >&2
226
- echo " See docs/architecture/DEVELOPER_GUIDE.md § Releasing." >&2
226
+ echo " See docs/architecture/platform/DEVELOPER_GUIDE.md § Releasing." >&2
227
227
  echo "" >&2
228
228
  exit 1
229
229
  else
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unoverse",
3
- "version": "0.1.199",
3
+ "version": "0.1.201",
4
4
  "description": "The Unoverse front door — create a Studio project, a universe, or a client app, and launch Studio.",
5
5
  "license": "SEE LICENSE IN README.md",
6
6
  "type": "module",
@@ -97,7 +97,8 @@ export function collectProject(designRoot, project) {
97
97
  // A prompt block is NEVER qualified this way — see the blocks/ walk below for why.
98
98
  // Apps keep their bare ids: those are org-qualified by convention
99
99
  // (`<org>-chat`). docs/unoverse/UNOVERSE_COMPONENT_ORGS.md.
100
- const QUALIFIED_KINDS = new Set(["component", "skill", "template"]);
100
+ // `identity` (2026-09-05): the org's four documents, `<org>/brand` like a component.
101
+ const QUALIFIED_KINDS = new Set(["component", "skill", "template", "identity"]);
101
102
  const add = (kind, name, definition) => items.push({
102
103
  kind,
103
104
  name: QUALIFIED_KINDS.has(kind) ? `${project}/${name}` : name,
@@ -121,7 +122,10 @@ export function collectProject(designRoot, project) {
121
122
  // 029): apps publish as kind `app` and the Template Model kind owns the bare
122
123
  // word. Forgetting a design dir here ships deploys that silently drop every
123
124
  // artifact of that kind.
124
- for (const [dir, kind] of [["components", "component"], ["apps", "app"], ["templates", "template"], ["atoms", "atom"]]) {
125
+ // IDENTITY (owner ruling 2026-09-05): design/<org>/identity/<doc>/ publishes as kind
126
+ // `identity`, the same walk as a component folder, so it reaches the registry, the
127
+ // spatial catalogue and the map through the one publish path everything else takes.
128
+ for (const [dir, kind] of [["components", "component"], ["apps", "app"], ["templates", "template"], ["atoms", "atom"], ["identity", "identity"]]) {
125
129
  const home = join(root, dir);
126
130
  if (!existsSync(home))
127
131
  continue;
@@ -10,7 +10,7 @@
10
10
  * new, changed, unchanged, or refused because the name belongs to someone else. Publishing
11
11
  * blind is how a project half-lands and nobody can tell by looking.
12
12
  *
13
- * See docs/architecture/DECLARATIVE_NODES.md §9.
13
+ * See docs/architecture/authoring/DECLARATIVE_NODES.md §9.
14
14
  */
15
15
  import { existsSync } from "node:fs";
16
16
  import { join } from "node:path";
@@ -57,7 +57,7 @@ export const PROP_KEYS = [
57
57
  */
58
58
  export const INTERFACE_FIELDS = new Set([
59
59
  "universal_id", "title", "description", "object_type", "key_need", "source_url", "source_id",
60
- "tagline", "shortDescription", "introParagraph", "callToAction", "actionPrompt", "primaryImage", "images", "action",
60
+ "tagline", "shortDescription", "introParagraph", "callToAction", "primaryImage", "images", "action",
61
61
  "bodyCopy", "features", "mainCategory", "section", "needs",
62
62
  ]);
63
63
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://unoverse/nodes/_defs.schema.json",
3
+ "$id": "https://docs.unoverse.ai/schemas/nodes/_defs.schema.json",
4
4
  "title": "Shared node definitions",
5
- "description": "Fragments $ref'd by the sibling node schemas. Not authored directly. See docs/architecture/DECLARATIVE_NODES.md.",
5
+ "description": "Fragments $ref'd by the sibling node schemas. Not authored directly. See docs/architecture/authoring/DECLARATIVE_NODES.md.",
6
6
  "definitions": {
7
7
  "nodeType": {
8
8
  "type": "string",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://unoverse/nodes/api.schema.json",
3
+ "$id": "https://docs.unoverse.ai/schemas/nodes/api.schema.json",
4
4
  "title": "Unoverse node API call",
5
- "description": "api.yaml — the upstream call this node makes, and how its response maps onto the node's outputs. This is the file that replaces an executor. NAMED api, not service: `service` already means serviceConnectors / isService / MCP providers / /service-call in this platform. See docs/architecture/DECLARATIVE_NODES.md §8.\n\nEVERY enum below is the EXECUTOR'S CAPABILITY LIST. A manifest may only name a capability that is already implemented in code. Adding a value here without implementing it produces a node that lints clean and fails at run time, and allowing a manifest to name arbitrary code would end the safety property this whole format exists for (§2).\n\nA node is reached two ways, and they never cross. The GRAPH RUNS it: `run` is the ordered list of calls it makes, and `events` says what lands on its output connectors. Or a SERVICE CALLS it ad-hoc, exposed as MCP: `service` holds those methods, each with its own list of calls, each handing one value straight back to the caller rather than touching a connector. A pure service node has only `service`.\n\nCalls are ALWAYS a `run` list, even when there is one of them. There is no `request` key.",
5
+ "description": "api.yaml — the upstream call this node makes, and how its response maps onto the node's outputs. This is the file that replaces an executor. NAMED api, not service: `service` already means serviceConnectors / isService / MCP providers / /service-call in this platform. See docs/architecture/authoring/DECLARATIVE_NODES.md §8.\n\nEVERY enum below is the EXECUTOR'S CAPABILITY LIST. A manifest may only name a capability that is already implemented in code. Adding a value here without implementing it produces a node that lints clean and fails at run time, and allowing a manifest to name arbitrary code would end the safety property this whole format exists for (§2).\n\nA node is reached two ways, and they never cross. The GRAPH RUNS it: `run` is the ordered list of calls it makes, and `events` says what lands on its output connectors. Or a SERVICE CALLS it ad-hoc, exposed as MCP: `service` holds those methods, each with its own list of calls, each handing one value straight back to the caller rather than touching a connector. A pure service node has only `service`.\n\nCalls are ALWAYS a `run` list, even when there is one of them. There is no `request` key.",
6
6
  "type": "object",
7
7
  "required": [],
8
8
  "definitions": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://unoverse/nodes/audio.schema.json",
3
+ "$id": "https://docs.unoverse.ai/schemas/nodes/audio.schema.json",
4
4
  "title": "Unoverse node audio lane",
5
5
  "description": "api/audio.yaml — how a duplex voice node is wired to the platform's audio lane. Only meaningful beside a `transport: ws` call.\n\nWHY THIS FILE EXISTS, and it is not a design preference. There are TWO sockets in a voice call. The VENDOR socket lives in api/run.yaml and is the node's conversation with OpenAI, xAI or Nova. The AUDIO LANE is the platform's own socket to the browser, and it exists for exactly ONE reason: MCP cannot carry binary audio.\n\nSo the rule that keeps this file small: EVERYTHING THAT IS NOT AUDIO BELONGS IN `events`. Transcripts, tool results, speech state, usage — all of it reaches the client over MCP streaming already, by landing on an output connector like any other node's output. Nothing needs a side channel to get there. If something non-audio is being put in this file, the reason is almost always that it was easier to reach the lane than to declare an output, and that is the wrong trade: an output is visible on the canvas, wirable, and testable, and a side channel is none of those.\n\nWHAT THE EXECUTOR CONTRIBUTES. Everything here names computation rather than describing data, which is why it is a handful of keys rather than a body template. Resampling is an interpolation over samples. Coalescing is a timer and a byte budget. Barge-in is discarding a buffer rather than flushing it. None of those can be written as an expression, and all of them are identical for every vendor, which is exactly the test for belonging to the executor (DECLARATIVE_NODES.md §2).",
6
6
  "type": "object",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://unoverse/nodes/config.schema.json",
3
+ "$id": "https://docs.unoverse.ai/schemas/nodes/config.schema.json",
4
4
  "title": "Unoverse node config",
5
- "description": "config.yaml \u2014 the node's settings form. `configSchema` is itself a JSON Schema: this file describes the SHAPE OF THAT SCHEMA, not the shape of a config. Canvas renders the form from it and the executor resolves {{ config.* }} against the saved values.\n\nThis is the file with the most churn: every new option lands here and nowhere else. Full reference for the options: docs-starter/nodes/config-schema.md.",
5
+ "description": "config.yaml the node's settings form. `configSchema` is itself a JSON Schema: this file describes the SHAPE OF THAT SCHEMA, not the shape of a config. Canvas renders the form from it and the executor resolves {{ config.* }} against the saved values.\n\nThis is the file with the most churn: every new option lands here and nowhere else. Full reference for the options: docs-starter/nodes/config-schema.md.",
6
6
  "type": "object",
7
7
  "required": [
8
8
  "configSchema"
@@ -116,7 +116,7 @@
116
116
  "enum": [
117
117
  "modelTier"
118
118
  ],
119
- "description": "An executor RESOLVER applied to this field's saved value just before the request is built. IMPLEMENTED resolvers only.\n\n`modelTier` is the migration of shared/models.ts resolveModel(): a saved workflow stores a concrete model id, and if that id is later retired the resolver falls back to the current best model in the same tier, warning the operator. Without it, replacing a model generation breaks every saved workflow that named an old id.\n\nThis is the \u00a72 line in miniature: the model LIST is description and lives in shared/models.yaml, while the fallback is computation and lives in the executor.",
119
+ "description": "An executor RESOLVER applied to this field's saved value just before the request is built. IMPLEMENTED resolvers only.\n\n`modelTier` is the migration of shared/models.ts resolveModel(): a saved workflow stores a concrete model id, and if that id is later retired the resolver falls back to the current best model in the same tier, warning the operator. Without it, replacing a model generation breaks every saved workflow that named an old id.\n\nThis is the §2 line in miniature: the model LIST is description and lives in shared/models.yaml, while the fallback is computation and lives in the executor.",
120
120
  "$comment": "Adding a value here without implementing it produces a node that lints clean and fails at run time."
121
121
  },
122
122
  "ui:field": {
@@ -137,7 +137,7 @@
137
137
  "checkboxes",
138
138
  "domainSelector"
139
139
  ],
140
- "description": "IMPLEMENTED widgets only, verified against the renderer in apps/canvas/src/components/workflow/ConfigurationForm/FieldRenderer.jsx:\n\n toggle a boolean switch\n select an enum picker\n textarea a multi-line box (FieldRenderer.jsx:248). Pair with ui:options.rows\n slider a number, dragged rather than typed (:162)\n checkboxes an array of enum values, all visible at once (:208)\n domainSelector an array of hostnames (:195)\n\nNOT `color`. The retired Note node declared `ui:widget: color` and the renderer has never implemented it, so it fell through to a plain text input \u2014 a setting that looked bespoke and was not. Adding a value here without a branch in FieldRenderer reproduces exactly that: it lints clean and silently renders as something else."
140
+ "description": "IMPLEMENTED widgets only, verified against the renderer in apps/canvas/src/components/workflow/ConfigurationForm/FieldRenderer.jsx:\n\n toggle a boolean switch\n select an enum picker\n textarea a multi-line box (FieldRenderer.jsx:248). Pair with ui:options.rows\n slider a number, dragged rather than typed (:162)\n checkboxes an array of enum values, all visible at once (:208)\n domainSelector an array of hostnames (:195)\n\nNOT `color`. The retired Note node declared `ui:widget: color` and the renderer has never implemented it, so it fell through to a plain text input a setting that looked bespoke and was not. Adding a value here without a branch in FieldRenderer reproduces exactly that: it lints clean and silently renders as something else."
141
141
  },
142
142
  "ui:dependencies": {
143
143
  "type": "object",
@@ -171,4 +171,4 @@
171
171
  }
172
172
  }
173
173
  }
174
- }
174
+ }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://unoverse/nodes/credential.schema.json",
3
+ "$id": "https://docs.unoverse.ai/schemas/nodes/credential.schema.json",
4
4
  "title": "Unoverse credential type",
5
- "description": "credentials/<name>.yaml \u2014 the SHAPE of a credential, never its value. Declared once per package and referenced by name from each node.yaml.\n\nSecrets stay where they already live: encrypted in Postgres via credentialManager on a deployed universe, and in the developer's own .env locally (LOCAL_STUDIO.md). Only the schema is data in a manifest, which is why a manifest is safe to commit and to deploy.",
5
+ "description": "credentials/<name>.yaml the SHAPE of a credential, never its value. Declared once per package and referenced by name from each node.yaml.\n\nSecrets stay where they already live: encrypted in Postgres via credentialManager on a deployed universe, and in the developer's own .env locally (LOCAL_STUDIO.md). Only the schema is data in a manifest, which is why a manifest is safe to commit and to deploy.",
6
6
  "type": "object",
7
7
  "required": [
8
8
  "name",
@@ -44,7 +44,7 @@
44
44
  "name": {
45
45
  "type": "string",
46
46
  "pattern": "^[a-z][A-Za-z0-9_]*$",
47
- "description": "The FIELD name, as a manifest reads it: credentials.<credentialName>.<fieldName>.\n\ncamelCase by convention. snake_case is ALLOWED, for exactly the reason a PORT name may be snake_case (_defs.schema.json): a migrated node must keep the names its code version used. A credential is read field by field out of stored, encrypted values \u2014 Cloudinary's are cloud_name, api_key and api_secret \u2014 so tidying them to camelCase would leave every existing workflow authenticating with undefined, and it would fail as a vendor 401 rather than as anything naming the rename. Fidelity beats house style here.\n\nPrefer camelCase for a NEW credential, where nothing is stored yet."
47
+ "description": "The FIELD name, as a manifest reads it: credentials.<credentialName>.<fieldName>.\n\ncamelCase by convention. snake_case is ALLOWED, for exactly the reason a PORT name may be snake_case (_defs.schema.json): a migrated node must keep the names its code version used. A credential is read field by field out of stored, encrypted values Cloudinary's are cloud_name, api_key and api_secret so tidying them to camelCase would leave every existing workflow authenticating with undefined, and it would fail as a vendor 401 rather than as anything naming the rename. Fidelity beats house style here.\n\nPrefer camelCase for a NEW credential, where nothing is stored yet."
48
48
  },
49
49
  "displayName": {
50
50
  "type": "string"
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://docs.unoverse.ai/schemas/nodes/events.schema.json",
4
+ "title": "api/events.yaml: one row per output connector",
5
+ "$ref": "api.schema.json#/properties/events"
6
+ }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://unoverse/nodes/interface.schema.json",
3
+ "$id": "https://docs.unoverse.ai/schemas/nodes/interface.schema.json",
4
4
  "title": "Unoverse node interface",
5
- "description": "interface.yaml — the node's WIRING SURFACE, and nothing else. Everything another node, an agent, or a developer needs in order to connect to this one: what goes in, what comes out, what services it offers or consumes, and what credentials it needs.\n\nIt has its own file because this is the question asked most often about a node, and it should never require reading past a node's branding to answer. See docs/architecture/DECLARATIVE_NODES.md §5.",
5
+ "description": "interface.yaml — the node's WIRING SURFACE, and nothing else. Everything another node, an agent, or a developer needs in order to connect to this one: what goes in, what comes out, what services it offers or consumes, and what credentials it needs.\n\nIt has its own file because this is the question asked most often about a node, and it should never require reading past a node's branding to answer. See docs/architecture/authoring/DECLARATIVE_NODES.md §5.",
6
6
  "type": "object",
7
7
  "properties": {
8
8
  "$schema": {
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://docs.unoverse.ai/schemas/nodes/narrate.schema.json",
4
+ "title": "api/narrate.yaml: a second model narrating progress",
5
+ "$ref": "api.schema.json#/properties/narrate"
6
+ }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://unoverse/nodes/node.schema.json",
3
+ "$id": "https://docs.unoverse.ai/schemas/nodes/node.schema.json",
4
4
  "title": "Unoverse node",
5
- "description": "node.yaml — what this node IS: its identity, its kind, and how it is discovered. The only required file in a node folder.\n\nThe wiring surface lives next door in interface.yaml, so \"what can I connect to this?\" is answered without reading past the node's branding. config/api/test/interface may each be inlined here instead of living in their own file, but never both (that is a lint error, not a merge). See docs/architecture/DECLARATIVE_NODES.md §5.",
5
+ "description": "node.yaml — what this node IS: its identity, its kind, and how it is discovered. The only required file in a node folder.\n\nThe wiring surface lives next door in interface.yaml, so \"what can I connect to this?\" is answered without reading past the node's branding. config/api/test/interface may each be inlined here instead of living in their own file, but never both (that is a lint error, not a merge). See docs/architecture/authoring/DECLARATIVE_NODES.md §5.",
6
6
  "type": "object",
7
7
  "required": [
8
8
  "type",
@@ -89,23 +89,31 @@
89
89
  "cacheable": {
90
90
  "description": "Output may be MEMOIZED: the engine may serve a prior run's output when the fingerprint (type + version + RESOLVED config + credential ref + scope) matches.\n\nSet ONLY for idempotent side-effect-free READS (search, scrape, fetch-by-id, pure transform). NEVER for effectful nodes (send, write, post, charge) where reuse silently skips the side effect, and NEVER for non-deterministic ones (LLM completions, time or random dependent) where re-running is the correct behaviour. Memoization is off engine-wide by default; this flag only makes the node eligible.\n\nOBJECT FORM, for nodes whose config carries a VOLATILE field (a presigned/expiring URL) over content whose identity arrives on an input (an etag). `ignore` drops the named top-level resolved-config fields from the fingerprint; `key` names input leaf fields (dot-suffix match, e.g. \"etag\" or \"file.etag\") the engine collects from the resolved inputs as the content's identity. If a key field collects NOTHING on a run, that run is not cached — no identity, no reuse. Declaring the object form IS the opt-in.",
91
91
  "oneOf": [
92
- { "type": "boolean" },
92
+ {
93
+ "type": "boolean"
94
+ },
93
95
  {
94
96
  "type": "object",
95
97
  "properties": {
96
98
  "ignore": {
97
99
  "type": "array",
98
- "items": { "type": "string" },
100
+ "items": {
101
+ "type": "string"
102
+ },
99
103
  "description": "Top-level resolved-config fields excluded from the fingerprint because they are volatile, not identity (a presigned URL that changes every run). Every OTHER config field still busts the cache."
100
104
  },
101
105
  "key": {
102
106
  "type": "array",
103
- "items": { "type": "string" },
107
+ "items": {
108
+ "type": "string"
109
+ },
104
110
  "minItems": 1,
105
111
  "description": "Input leaf fields that identify the content, matched by dot-suffix against every leaf path in the resolved inputs (wiring-independent: \"etag\" matches inputs.file.<anyNode>.file.etag). All matches, sorted by path, enter the fingerprint. A key that matches nothing disables caching for that run."
106
112
  }
107
113
  },
108
- "required": ["key"],
114
+ "required": [
115
+ "key"
116
+ ],
109
117
  "additionalProperties": false
110
118
  }
111
119
  ]
@@ -135,7 +143,7 @@
135
143
  },
136
144
  "auth": {
137
145
  "type": "object",
138
- "description": "WHO MAY RUN THIS NODE — inbound, about the caller (docs/architecture/DECLARATIVE_NODES.md §9.13). Not to be confused with a call's `credential` in api/run.yaml, which is outbound: how the node proves itself to a vendor. Both were spelled `auth` until 2026-07-28.\n\nCOMPULSORY, which is the point. It was optional (`requires: { role }`) until 2026-07-28, and every node in the tree said nothing — so a node that had been considered and left open looked exactly like a node nobody had thought about. Silence is not an answer to this question.",
146
+ "description": "WHO MAY RUN THIS NODE — inbound, about the caller (docs/architecture/authoring/DECLARATIVE_NODES.md §9.13). Not to be confused with a call's `credential` in api/run.yaml, which is outbound: how the node proves itself to a vendor. Both were spelled `auth` until 2026-07-28.\n\nCOMPULSORY, which is the point. It was optional (`requires: { role }`) until 2026-07-28, and every node in the tree said nothing — so a node that had been considered and left open looked exactly like a node nobody had thought about. Silence is not an answer to this question.",
139
147
  "additionalProperties": false,
140
148
  "properties": {
141
149
  "required": {
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://unoverse/nodes/package.schema.json",
3
+ "$id": "https://docs.unoverse.ai/schemas/nodes/package.schema.json",
4
4
  "title": "Unoverse node package",
5
- "description": "package.yaml \u2014 the envelope for a folder of nodes. Replaces the `gravity` block in package.json for packages that no longer ship code. See docs/architecture/DECLARATIVE_NODES.md \u00a75.",
5
+ "description": "package.yaml the envelope for a folder of nodes. Replaces the `gravity` block in package.json for packages that no longer ship code. See docs/architecture/authoring/DECLARATIVE_NODES.md §5.",
6
6
  "type": "object",
7
7
  "required": [
8
8
  "name",
@@ -61,7 +61,7 @@
61
61
  "type": "string",
62
62
  "pattern": "^(\\*$|(\\*\\*\\.|\\*\\.)?[a-z0-9-]+(\\.[a-z0-9-]+)+$)"
63
63
  },
64
- "description": "The ONLY hosts this package's nodes may call. The executor refuses any other, and lint refuses a request URL outside it.\n\nThis is the control that makes a manifest safe to accept from someone else. A manifest cannot execute, but it CAN say \"POST this credential to evil.example\", and that is exfiltration without a line of code. Bounding allowedHosts means a package's secrets can only ever reach hosts it declared in writing, and the declaration is one short reviewable list at install time.\n\nSECURITY.md draws the line as: a NODE is trusted code bounded by provenance; a TEMPLATE EXPRESSION is untrusted data bounded by having no credentials in scope. A manifest node is neither \u2014 it arrives as data but holds a credential and a URL. AllowedHosts is its boundary.\n\n`*.` matches ONE subdomain level, e.g. \"*.slack.com\" reaches api.slack.com but not a.b.slack.com.\n\n`**.` matches ANY depth, and is written with two stars because it is a deliberately weaker claim the author should have to make on purpose. AWS forced it: `dynamodb.us-east-1.amazonaws.com` is two labels deep and an S3 bucket is three, since the region AND the bucket are part of the host. Listing every region defeats itself the day AWS adds one, and a bucket name comes from config so it cannot be listed at all. Still bounded: \"**.amazonaws.com\" reaches any AWS host and nothing else.\n\n`*` alone means ANY HOST, and it applies ONLY to calls that send no credential. Some nodes legitimately fetch a url a person supplies (an image for a model to look at, a document to read) and that url cannot be declared in advance. It is safe exactly there: this boundary exists to stop exfiltration, and a call with no `auth` block has no credential to leak. Non-https is still refused, so cloud metadata and plaintext internal services stay out of reach, and the moment a call carries a credential the wildcard stops matching and the declared list applies.",
64
+ "description": "The ONLY hosts this package's nodes may call. The executor refuses any other, and lint refuses a request URL outside it.\n\nThis is the control that makes a manifest safe to accept from someone else. A manifest cannot execute, but it CAN say \"POST this credential to evil.example\", and that is exfiltration without a line of code. Bounding allowedHosts means a package's secrets can only ever reach hosts it declared in writing, and the declaration is one short reviewable list at install time.\n\nSECURITY.md draws the line as: a NODE is trusted code bounded by provenance; a TEMPLATE EXPRESSION is untrusted data bounded by having no credentials in scope. A manifest node is neither it arrives as data but holds a credential and a URL. AllowedHosts is its boundary.\n\n`*.` matches ONE subdomain level, e.g. \"*.slack.com\" reaches api.slack.com but not a.b.slack.com.\n\n`**.` matches ANY depth, and is written with two stars because it is a deliberately weaker claim the author should have to make on purpose. AWS forced it: `dynamodb.us-east-1.amazonaws.com` is two labels deep and an S3 bucket is three, since the region AND the bucket are part of the host. Listing every region defeats itself the day AWS adds one, and a bucket name comes from config so it cannot be listed at all. Still bounded: \"**.amazonaws.com\" reaches any AWS host and nothing else.\n\n`*` alone means ANY HOST, and it applies ONLY to calls that send no credential. Some nodes legitimately fetch a url a person supplies (an image for a model to look at, a document to read) and that url cannot be declared in advance. It is safe exactly there: this boundary exists to stop exfiltration, and a call with no `auth` block has no credential to leak. Non-https is still refused, so cloud metadata and plaintext internal services stay out of reach, and the moment a call carries a credential the wildcard stops matching and the declared list applies.",
65
65
  "examples": [
66
66
  [
67
67
  "api.openai.com"
@@ -133,7 +133,7 @@
133
133
  },
134
134
  "poll": {
135
135
  "type": "boolean",
136
- "description": "True when a node waits on an asynchronous JOB: start it, then ask until it is done. On an executor without it the node settles on the START reply, which is a receipt carrying a job id, and emits that as its answer \u2014 every downstream field reads empty and nothing errors."
136
+ "description": "True when a node waits on an asynchronous JOB: start it, then ask until it is done. On an executor without it the node settles on the START reply, which is a receipt carrying a job id, and emits that as its answer every downstream field reads empty and nothing errors."
137
137
  },
138
138
  "encoding": {
139
139
  "type": "array",
@@ -145,7 +145,7 @@
145
145
  "ndjson"
146
146
  ]
147
147
  },
148
- "description": "Body encodings this package's nodes use. Declared like auth and transport, and for the same reason: on an executor without one, the body is sent as plain JSON instead. That is not an error anywhere \u2014 the vendor simply rejects a shape it did not expect, or worse accepts it and ignores the file, so an upload silently transcribes nothing."
148
+ "description": "Body encodings this package's nodes use. Declared like auth and transport, and for the same reason: on an executor without one, the body is sent as plain JSON instead. That is not an error anywhere the vendor simply rejects a shape it did not expect, or worse accepts it and ignores the file, so an upload silently transcribes nothing."
149
149
  },
150
150
  "renderComponents": {
151
151
  "type": "boolean",
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://docs.unoverse.ai/schemas/nodes/run.schema.json",
4
+ "title": "api/run.yaml: the calls a node makes, always a list",
5
+ "$ref": "api.schema.json#/properties/run"
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://docs.unoverse.ai/schemas/nodes/service.schema.json",
4
+ "title": "api/service.yaml: methods offered over a service edge",
5
+ "$ref": "api.schema.json#/properties/service"
6
+ }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://unoverse/nodes/test.schema.json",
3
+ "$id": "https://docs.unoverse.ai/schemas/nodes/test.schema.json",
4
4
  "title": "Unoverse node test data",
5
- "description": "test.yaml \u2014 the fixture behind Studio's \"Load sample, Run\" button and behind `unoverse node test`. Every node must have one: a node nobody can run is a node nobody can trust, and lint enforces it.\n\nTemplate fields are supplied ALREADY RESOLVED, because the bench has no upstream node to resolve them from. See docs/architecture/DECLARATIVE_NODES.md \u00a76.",
5
+ "description": "test.yaml the fixture behind Studio's \"Load sample, Run\" button and behind `unoverse node test`. Every node must have one: a node nobody can run is a node nobody can trust, and lint enforces it.\n\nTemplate fields are supplied ALREADY RESOLVED, because the bench has no upstream node to resolve them from. See docs/architecture/authoring/DECLARATIVE_NODES.md §6.",
6
6
  "type": "object",
7
7
  "required": [
8
8
  "testData"
@@ -42,11 +42,11 @@
42
42
  "additionalProperties": false
43
43
  },
44
44
  "state": {
45
- "type": "object",
46
- "description": "PLATFORM STORAGE this fixture starts with, keyed by LOGICAL key. The bench has no Redis, so it runs the real state and loop code against an in-memory client seeded from here; a value is either an object (stored whole) or an array (stored as a list, for drain).\n\nIt exists because some nodes only make sense with a warm store. `LoopEnd` closes a pass of a loop that `LoopStart` opened, so with nothing seeded its very first act is the \"loop state not found\" error it is supposed to raise for a mistyped id \u2014 the bench would report a real bug as the node's normal behaviour. Same for any node whose first call is a cache read.\n\nThe run's ids are FIXED on the bench (see test-node.mjs) so a key naming them is predictable.",
47
- "additionalProperties": true
48
- },
49
- "expect": {
45
+ "type": "object",
46
+ "description": "PLATFORM STORAGE this fixture starts with, keyed by LOGICAL key. The bench has no Redis, so it runs the real state and loop code against an in-memory client seeded from here; a value is either an object (stored whole) or an array (stored as a list, for drain).\n\nIt exists because some nodes only make sense with a warm store. `LoopEnd` closes a pass of a loop that `LoopStart` opened, so with nothing seeded its very first act is the \"loop state not found\" error it is supposed to raise for a mistyped id the bench would report a real bug as the node's normal behaviour. Same for any node whose first call is a cache read.\n\nThe run's ids are FIXED on the bench (see test-node.mjs) so a key naming them is predictable.",
47
+ "additionalProperties": true
48
+ },
49
+ "expect": {
50
50
  "type": "object",
51
51
  "description": "Assertions over the result. On the workflow channel the scope is `output` (the emitted connectors); on the service channel it is `output` too, bound to the value the method RETURNED. Absent means the run only has to succeed.",
52
52
  "additionalProperties": {
@@ -58,7 +58,7 @@
58
58
  "required": [
59
59
  "method"
60
60
  ],
61
- "description": "For a node with `provides`: which SERVICE method to exercise and with what arguments. A pure service node has no inputs to feed and no outputs to watch, so `inputs` cannot stand in \u2014 the only way to run it is to call it the way a consumer would.",
61
+ "description": "For a node with `provides`: which SERVICE method to exercise and with what arguments. A pure service node has no inputs to feed and no outputs to watch, so `inputs` cannot stand in the only way to run it is to call it the way a consumer would.",
62
62
  "properties": {
63
63
  "method": {
64
64
  "type": "string",
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://docs.unoverse.ai/schemas/nodes/toolExchange.schema.json",
4
+ "title": "api/toolExchange.yaml: how this API expresses a tool call",
5
+ "$ref": "api.schema.json#/properties/toolExchange"
6
+ }
@@ -13,7 +13,7 @@
13
13
  * without it is the gap LOCAL_STUDIO.md:246 names: "a malformed definition is one restart
14
14
  * from a broken server", and a published row does not even need the restart.
15
15
  *
16
- * FOUR TIERS (docs/architecture/DECLARATIVE_NODES.md §6). This is tiers 2 and 3.
16
+ * FOUR TIERS (docs/architecture/authoring/DECLARATIVE_NODES.md §6). This is tiers 2 and 3.
17
17
  * 1 editor $schema pointers + .vscode yaml.schemas, no tool
18
18
  * 2 structural every part validated against the node schemas (packages/docs/schemas/nodes)
19
19
  * 3 semantic the cross-file rules a schema cannot express
@@ -25,13 +25,13 @@ try {
25
25
  }
26
26
 
27
27
  export const SCHEMA_ID = {
28
- node: "https://unoverse/nodes/node.schema.json",
29
- interface: "https://unoverse/nodes/interface.schema.json",
30
- config: "https://unoverse/nodes/config.schema.json",
31
- api: "https://unoverse/nodes/api.schema.json",
32
- test: "https://unoverse/nodes/test.schema.json",
33
- package: "https://unoverse/nodes/package.schema.json",
34
- credential: "https://unoverse/nodes/credential.schema.json",
28
+ node: "https://docs.unoverse.ai/schemas/nodes/node.schema.json",
29
+ interface: "https://docs.unoverse.ai/schemas/nodes/interface.schema.json",
30
+ config: "https://docs.unoverse.ai/schemas/nodes/config.schema.json",
31
+ api: "https://docs.unoverse.ai/schemas/nodes/api.schema.json",
32
+ test: "https://docs.unoverse.ai/schemas/nodes/test.schema.json",
33
+ package: "https://docs.unoverse.ai/schemas/nodes/package.schema.json",
34
+ credential: "https://docs.unoverse.ai/schemas/nodes/credential.schema.json",
35
35
  };
36
36
 
37
37
  /** The four sections that may live in their own file OR inline in node.yaml. */
@@ -9,7 +9,7 @@
9
9
  * Everything past this file works on the composed definition and never learns where
10
10
  * it came from.
11
11
  *
12
- * See docs/architecture/DECLARATIVE_NODES.md.
12
+ * See docs/architecture/authoring/DECLARATIVE_NODES.md.
13
13
  */
14
14
  import * as fs from "fs";
15
15
  import * as path from "path";