@unoverse-platform/studio 0.1.97 → 0.1.98

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 (118) hide show
  1. package/bin/studio.mjs +2 -2
  2. package/local/catalog.ts +5 -1
  3. package/local/nodes.ts +28 -41
  4. package/local/plugin.ts +71 -13
  5. package/local/sample-identity/brand.yaml +24 -0
  6. package/local/sample-identity/organisation.yaml +14 -0
  7. package/local/sample-identity/purpose.yaml +14 -0
  8. package/local/sample-identity/story.yaml +18 -0
  9. package/local/scaffold.mjs +43 -23
  10. package/local/source.ts +3 -8
  11. package/package.json +1 -1
  12. package/screens/AgentsView.tsx +173 -0
  13. package/screens/ComponentsView.tsx +2 -2
  14. package/screens/EmptyLibrary.tsx +1 -1
  15. package/screens/IdentityView.tsx +3 -3
  16. package/screens/PromptBlocksView.tsx +1 -1
  17. package/screens/TemplatesLibraryView.tsx +91 -6
  18. package/screens/TemplatesView.tsx +24 -9
  19. package/screens/index.ts +2 -0
  20. package/src/App.tsx +8 -3
  21. package/src/host.ts +1 -14
  22. package/vendor/base/src/agent-mcp/discovery.ts +76 -18
  23. package/vendor/base/src/agent-mcp/handoff.ts +11 -31
  24. package/vendor/base/src/agent-mcp/index.ts +5 -5
  25. package/vendor/base/src/agent-mcp/invoke.ts +37 -19
  26. package/vendor/base/src/agent-mcp/lean.ts +19 -0
  27. package/vendor/base/src/agent-mcp/memoryContext.ts +15 -9
  28. package/vendor/base/src/agent-mcp/memoryIngest.ts +8 -3
  29. package/vendor/base/src/agent-mcp/rowComponents.ts +184 -72
  30. package/vendor/base/src/agent-mcp/tools.ts +136 -7
  31. package/vendor/base/src/agent-mcp/types.ts +47 -10
  32. package/vendor/base/src/definitions/load/compile/component.ts +59 -4
  33. package/vendor/base/src/definitions/load/compile/sections.ts +7 -1
  34. package/vendor/base/src/definitions/load/compile/template.ts +19 -15
  35. package/vendor/base/src/definitions/load/expand/inherit.ts +5 -1
  36. package/vendor/base/src/definitions/load/expand/referenced.ts +21 -2
  37. package/vendor/base/src/definitions/load/expand/refs.ts +14 -0
  38. package/vendor/base/src/definitions/load/expand/walk.ts +88 -38
  39. package/vendor/base/src/definitions/load/load.ts +37 -28
  40. package/vendor/base/src/definitions/model.ts +8 -12
  41. package/vendor/base/src/definitions/registry/agents.ts +111 -0
  42. package/vendor/base/src/definitions/registry/catalog.ts +28 -12
  43. package/vendor/base/src/definitions/registry/datasetSql.ts +149 -0
  44. package/vendor/base/src/definitions/registry/homes.ts +69 -5
  45. package/vendor/base/src/definitions/registry/objects.ts +413 -0
  46. package/vendor/base/src/definitions/registry/orgDocuments.ts +69 -29
  47. package/vendor/base/src/definitions/registry/orgPack.ts +11 -11
  48. package/vendor/base/src/definitions/registry/pipelines.ts +197 -0
  49. package/vendor/base/src/definitions/theme.ts +1 -2
  50. package/vendor/base/src/items/collect.ts +29 -25
  51. package/vendor/base/src/items/publish.ts +30 -6
  52. package/vendor/base/src/lint/context.mjs +17 -0
  53. package/vendor/base/src/lint/design/file.mjs +199 -2
  54. package/vendor/base/src/lint/design/index.mjs +66 -73
  55. package/vendor/base/src/lint/design/vocabulary.mjs +5 -2
  56. package/vendor/base/src/lint/design/walk.mjs +14 -4
  57. package/vendor/base/src/lint/index.mjs +15 -9
  58. package/vendor/base/src/lint/nodes/blocks.mjs +6 -2
  59. package/vendor/base/src/lint/nodes/index.mjs +5 -0
  60. package/vendor/base/src/lint/nodes/node.mjs +157 -7
  61. package/vendor/base/src/lint/objects/index.d.mts +32 -0
  62. package/vendor/base/src/lint/objects/index.mjs +324 -0
  63. package/vendor/base/src/lint/objects/rules.d.mts +44 -0
  64. package/vendor/base/src/lint/objects/rules.mjs +446 -0
  65. package/vendor/base/src/lint/prompts/index.d.mts +34 -0
  66. package/vendor/base/src/lint/prompts/index.mjs +198 -0
  67. package/vendor/base/src/manifests/compose.ts +80 -9
  68. package/vendor/base/src/manifests/executor/callback.ts +15 -4
  69. package/vendor/base/src/manifests/executor/context.ts +9 -1
  70. package/vendor/base/src/manifests/executor/memory.ts +83 -3
  71. package/vendor/base/src/manifests/executor/session.ts +50 -0
  72. package/vendor/base/src/manifests/executor/toolBridge.ts +114 -49
  73. package/vendor/base/src/manifests/runtime/auth/aws.ts +14 -1
  74. package/vendor/base/src/manifests/runtime/channels/final.ts +46 -34
  75. package/vendor/base/src/manifests/runtime/channels/service.ts +30 -32
  76. package/vendor/base/src/manifests/runtime/channels/workflow.ts +6 -11
  77. package/vendor/base/src/manifests/runtime/context.ts +18 -6
  78. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +159 -3
  79. package/vendor/base/src/manifests/runtime/duplex/session.ts +304 -9
  80. package/vendor/base/src/manifests/runtime/events.ts +39 -3
  81. package/vendor/base/src/manifests/runtime/http/concurrency.ts +44 -0
  82. package/vendor/base/src/manifests/runtime/http/request.ts +36 -7
  83. package/vendor/base/src/manifests/runtime/index.ts +3 -2
  84. package/vendor/base/src/manifests/runtime/loops/repeat.ts +86 -0
  85. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +30 -3
  86. package/vendor/base/src/manifests/runtime/usage.ts +10 -0
  87. package/vendor/base/src/paths.ts +16 -8
  88. package/vendor/base/src/platform/audioManager.ts +32 -0
  89. package/vendor/base/src/platform/executionContext.ts +14 -0
  90. package/vendor/base/src/platform/liveInput.ts +52 -0
  91. package/vendor/base/src/plugins/discovery.ts +1 -1
  92. package/vendor/base/src/template/SafeExpression.ts +6 -0
  93. package/vendor/base/src/workflow/GetMemoryService.ts +12 -11
  94. package/vendor/base/src/workflow/PlatformMemoryCore.ts +23 -10
  95. package/vendor/sdk/appEngine.tsx +30 -11
  96. package/vendor/sdk/lib/UnoverseComponent.tsx +8 -4
  97. package/vendor/sdk/lib/app.tsx +70 -95
  98. package/vendor/sdk/lib/connection.tsx +47 -57
  99. package/vendor/sdk/lib/core/actions.ts +6 -0
  100. package/vendor/sdk/lib/core/apps.ts +66 -81
  101. package/vendor/sdk/lib/core/client.ts +5 -14
  102. package/vendor/sdk/lib/core/connection.ts +30 -0
  103. package/vendor/sdk/lib/core/index.ts +2 -2
  104. package/vendor/sdk/lib/core/store.ts +90 -15
  105. package/vendor/sdk/lib/core/types.ts +9 -8
  106. package/vendor/sdk/lib/primitives.tsx +113 -4
  107. package/vendor/sdk/lib/realtime/useAudioPlayback.ts +8 -1
  108. package/vendor/sdk/lib/render.tsx +3 -3
  109. package/vendor/sdk/lib/voice.tsx +27 -13
  110. package/local/sample-identity/brand/brand.yaml +0 -88
  111. package/local/sample-identity/brand/manifest.yaml +0 -13
  112. package/local/sample-identity/organisation/manifest.yaml +0 -15
  113. package/local/sample-identity/organisation/organisation.yaml +0 -53
  114. package/local/sample-identity/purpose/manifest.yaml +0 -12
  115. package/local/sample-identity/purpose/purpose.yaml +0 -46
  116. package/local/sample-identity/story/manifest.yaml +0 -12
  117. package/local/sample-identity/story/story.yaml +0 -63
  118. package/vendor/base/src/definitions/registry/identitySeed.ts +0 -17
@@ -0,0 +1,446 @@
1
+ /**
2
+ * THE RULES an object or a pipeline file answers to, as PURE FUNCTIONS: a parsed document
3
+ * in, findings out. No filesystem, no schema, no dependency.
4
+ *
5
+ * ONE SPELLING OF EVERY RULE (docs/unoverse/data/objects/README.md 9a, 2026-09-10). The loaders
6
+ * in definitions/registry (objects.ts, pipelines.ts) used to refuse a wrong file by
7
+ * throwing their own sentence, and the linter would have said the same thing in other
8
+ * words. Now the loaders call these and throw the first finding; the linter reports every
9
+ * finding. The engine at boot and the editor at the keystroke say the same sentence about
10
+ * the same mistake.
11
+ *
12
+ * A finding is `{ file, msg }` where `file` is the folder-relative name of the file the
13
+ * mistake is in (`deal.yaml`, `props.yaml`, `api/service.yaml`, `pipeline.yaml`), so a
14
+ * caller that knows the folder can point at the line's file, and a caller that does not
15
+ * (the loader's thrown error) still names it. `refuses: true` marks the findings a LOADER
16
+ * cannot read past: the grammar itself (the type word, the name, the shape of calls and
17
+ * make). The rest (a missing face, a brief with no input, an unknown node) is the linter's
18
+ * to report and the engine's to tolerate at boot: a lint error blocks a publish, never a
19
+ * running universe.
20
+ */
21
+
22
+ export const KEBAB = /^[a-z0-9]+(-[a-z0-9]+)*$/;
23
+ const REF_OR_EXPR = /[{}]|^\s*return\b/;
24
+
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"]);
27
+
28
+ /** The face every object and pipeline carries, as a node's node.yaml does. */
29
+ export const FACE_KEYS = ["title", "description", "whenToUse", "category", "version"];
30
+
31
+ /** The prop grammar of UNOVERSE_AUTHORING 3b plus the object's one word, `from` (README 4c). */
32
+ export const PROP_KEYS = new Set(["type", "input", "description", "from", "hydrate", "enum", "oneOf", "minLength", "maxLength", "minItems", "maxItems", "items", "reuse"]);
33
+ export const LIMIT_KEYS = ["minLength", "maxLength", "minItems", "maxItems"];
34
+
35
+ /** Words that left the object grammar and must not come back (README 11.7, 12.3, 12.4). */
36
+ const DROPPED_ENVELOPE_KEYS = {
37
+ lifecycle: "an object has functions in api/service.yaml, never a lifecycle (README 11.7)",
38
+ credentials: "a credential is never passed; the node a call names declares its type and the org's one credential of that type is found (README 12.4)",
39
+ allowedHosts: "an object names no host; the node a call names carries its hosts (README 12.3)",
40
+ cardinality: "how many one source makes is the pipeline's: each entry of its make says cardinality (one or many), not the object (docs/unoverse/data/pipelines/README.md, layer 1)",
41
+ manifest: "the envelope IS the manifest: title, description, whenToUse, category, version sit here (README 4a)",
42
+ };
43
+
44
+ const isRecord = (x) => !!x && typeof x === "object" && !Array.isArray(x);
45
+
46
+ // ─────────────────────────────────────────────────────────────────────────────
47
+ // OBJECTS
48
+ // ─────────────────────────────────────────────────────────────────────────────
49
+
50
+ /**
51
+ * The envelope of object `name` (its `<name>.yaml`), before `extends` is composed.
52
+ * `file` is the envelope's file name, for the findings.
53
+ */
54
+ export function envelopeFindings(name, envelope, file = `${name}.yaml`) {
55
+ const out = [];
56
+ const say = (msg) => out.push({ file, msg });
57
+ const refuse = (msg) => out.push({ file, msg, refuses: true });
58
+ if (!isRecord(envelope)) return [{ file, msg: `object ${name}: the envelope must be a record`, refuses: true }];
59
+ if (envelope.type !== "object") refuse(`object ${name}: envelope must declare type: object`);
60
+ if (envelope.name !== name) refuse(`object ${name}: name must equal the folder`);
61
+ if (!KEBAB.test(name)) say(`object ${name}: the name is kebab-case: it is the row's object_type and the folder's name`);
62
+ for (const k of FACE_KEYS) if (typeof envelope[k] !== "string" || !envelope[k].trim()) say(`object ${name}: the face needs "${k}": what this is and when an Agent reaches for it, as a node's node.yaml (README 4a)`);
63
+ if (envelope.element != null && (typeof envelope.element !== "string" || !ELEMENTS.has(envelope.element))) say(`object ${name}: element "${envelope.element}" is not one of the twelve elements (${[...ELEMENTS].join(", ")}). It says what the object IS, in the vocabulary's own word (README 4a)`);
64
+ if (envelope.icon != null && (typeof envelope.icon !== "string" || !KEBAB.test(envelope.icon))) say(`object ${name}: icon "${envelope.icon}" is not a lucide icon name in kebab-case (file-text)`);
65
+ if (envelope.storedAs != null && !/^[a-z_]+$/.test(String(envelope.storedAs))) say(`object ${name}: storedAs is one word, the stored object_type, not "${envelope.storedAs}"`);
66
+ if (envelope.extends != null && typeof envelope.extends !== "string") refuse(`object ${name}: extends names an object by name`);
67
+ else if (envelope.extends != null && !KEBAB.test(envelope.extends)) say(`object ${name}: extends names an object by its kebab-case name, not "${envelope.extends}"`);
68
+ if (envelope.embed != null && (!Array.isArray(envelope.embed) || !envelope.embed.length || envelope.embed.some((f) => typeof f !== "string"))) refuse(`object ${name}: embed is a list of field names`);
69
+ // ONE KEY, OR SEVERAL IN ORDER (README 4a): a flat list of fields, or a list of such lists.
70
+ if (envelope.uniqueBy != null) {
71
+ const u = envelope.uniqueBy;
72
+ const flat = Array.isArray(u) && u.length > 0 && u.every((f) => typeof f === "string");
73
+ const ordered = Array.isArray(u) && u.length > 0 && u.every((k) => Array.isArray(k) && k.length > 0 && k.every((f) => typeof f === "string"));
74
+ if (!flat && !ordered) refuse(`object ${name}: uniqueBy is a list of field names (one key), or a list of such lists (several keys, tried in order)`);
75
+ }
76
+ if (envelope.search != null && envelope.search !== "all" && (!Array.isArray(envelope.search) || envelope.search.some((f) => typeof f !== "string"))) refuse(`object ${name}: search is a list of field names, or "all"`);
77
+ // NEVER A ROW (README 7): an object with no embed has no place on the map, so it can
78
+ // have no identity to match on and nothing a search hit could carry.
79
+ if (envelope.embed == null) {
80
+ if (envelope.uniqueBy != null) say(`object ${name}: declares uniqueBy but no embed; an object with no embed is never positioned, and a part's identity is the row it is part of (README 7)`);
81
+ if (envelope.search != null) say(`object ${name}: declares search but no embed; an object with no embed is never searched: a part is walked to (README 7)`);
82
+ } else if (Array.isArray(envelope.search) && envelope.search[0] !== "universal_id") say(`object ${name}: search lists universal_id first; a search hit is addressed by it (README 9a)`);
83
+ for (const [k, why] of Object.entries(DROPPED_ENVELOPE_KEYS)) if (k in envelope) say(`object ${name}: "${k}" is not a word here: ${why}`);
84
+ // ITS LINES (relationships-and-labels.md): by name, each a type, the fields that group its
85
+ // rows, the field that orders them, and when a row draws it.
86
+ if (envelope.links != null) {
87
+ if (!isRecord(envelope.links)) refuse(`object ${name}: links is a record of lines by name`);
88
+ else for (const [ln, l] of Object.entries(envelope.links)) {
89
+ if (!isRecord(l)) { refuse(`object ${name}: link ${ln} is a record: type, between, order, when; or type, to, match`); continue; }
90
+ if (!["link", "flow", "tree"].includes(l.type)) refuse(`object ${name}: link ${ln}: type is link, flow or tree, not "${l.type}"`);
91
+ if (l.to != null || l.match != null) {
92
+ // A LINE TO ANOTHER OBJECT (relationships-and-labels.md): the object it points at, and one
93
+ // pair of fields: this row's naming that row's.
94
+ if (typeof l.to !== "string" || !KEBAB.test(l.to)) refuse(`object ${name}: link ${ln}: to names the object the line points at, by its kebab-case name`);
95
+ const pairs = isRecord(l.match) ? Object.entries(l.match) : [];
96
+ if (pairs.length !== 1 || pairs.some(([a, b]) => typeof a !== "string" || typeof b !== "string" || !a || !b)) refuse(`object ${name}: link ${ln}: match is one pair, this row's field naming that row's ({ manager: legalName })`);
97
+ if (l.between != null || l.order != null) refuse(`object ${name}: link ${ln}: a line to another object says to and match, never between or order`);
98
+ if (l.alike != null && (!Array.isArray(l.alike) || l.alike.some((f) => typeof f !== "string" || !pairs.some(([a]) => a === f)))) refuse(`object ${name}: link ${ln}: alike lists this row's match field whose value may name one thing several ways`);
99
+ for (const k of Object.keys(l)) if (!["type", "to", "match", "when", "alike"].includes(k)) say(`object ${name}: link ${ln}: "${k}" is not a word here`);
100
+ continue;
101
+ }
102
+ if (!Array.isArray(l.between) || !l.between.length || l.between.some((f) => typeof f !== "string")) refuse(`object ${name}: link ${ln}: between is a list of the fields whose equal values make one line of rows`);
103
+ if (typeof l.order !== "string" || !l.order) refuse(`object ${name}: link ${ln}: order names the field its rows are put in order by`);
104
+ if (l.when != null && typeof l.when !== "string") refuse(`object ${name}: link ${ln}: when is an expression over the row`);
105
+ if (l.alike != null && (!Array.isArray(l.alike) || l.alike.some((f) => typeof f !== "string" || !(l.between ?? []).includes(f)))) refuse(`object ${name}: link ${ln}: alike lists fields of between whose values may name one thing several ways`);
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
+ }
108
+ }
109
+ return out;
110
+ }
111
+
112
+ /**
113
+ * The props an object declares ITSELF (props.yaml, or the envelope's inline record), with
114
+ * the parent's composed props when the object extends one. Every own prop is a brief with
115
+ * an explicit input; an inherited field is re-briefed with description alone.
116
+ */
117
+ export function propsFindings(name, ownProps, parentProps = null, base = null, file = "props.yaml") {
118
+ const out = [];
119
+ const say = (msg) => out.push({ file, msg });
120
+ if (ownProps == null) return out;
121
+ if (!isRecord(ownProps)) return [{ file, msg: `object ${name}: props is a record, one prop per field`, refuses: true }];
122
+ for (const [k, raw] of Object.entries(ownProps)) {
123
+ if (!isRecord(raw)) {
124
+ out.push({ file, msg: `object ${name}: prop "${k}" must be a record: a brief with a type and an input`, refuses: true });
125
+ continue;
126
+ }
127
+ const inherited = parentProps && k in parentProps;
128
+ if (inherited) {
129
+ // RE-BRIEF (README 5): an object may say its own brief for a field it inherits, and
130
+ // nothing else about it. A product's title is the offering's name, not the need's outcome.
131
+ const extra = Object.keys(raw).filter((key) => key !== "description");
132
+ if (extra.length) out.push({ file, refuses: true, msg: `object ${name}: redeclares "${k}", which its base ${base} already declares; an inherited field may only be re-briefed (description), not ${extra.join(", ")}` });
133
+ if (typeof raw.description !== "string" || !raw.description.trim()) say(`object ${name}: prop "${k}" re-briefs an inherited field, so it carries a description`);
134
+ continue;
135
+ }
136
+ for (const key of Object.keys(raw)) if (!PROP_KEYS.has(key)) say(`object ${name}: prop "${k}" carries "${key}", which is not a word of the prop grammar (${[...PROP_KEYS].join(", ")}); an object is a kind, never an instance, so no default or preview (README 4c)`);
137
+ if (typeof raw.description !== "string" || !raw.description.trim()) say(`object ${name}: prop "${k}" carries no description. The prop IS the brief: the description is the whole instruction for the field (README 4c)`);
138
+ if (typeof raw.input !== "boolean") say(`object ${name}: prop "${k}" says no input. Every own prop says who writes it: input: true, the model from the source; input: false, the engine or a function (README 4c)`);
139
+ for (const lim of LIMIT_KEYS) if (raw[lim] != null && !Number.isInteger(raw[lim])) say(`object ${name}: prop "${k}": ${lim} is a number, not "${raw[lim]}"`);
140
+ // A CHOICE LISTS ITS OPTIONS (README 4c, 2026-09-13): each a const, a title and a description; never with enum.
141
+ if (raw.oneOf != null) {
142
+ if (!Array.isArray(raw.oneOf) || !raw.oneOf.length || raw.oneOf.some((o) => !isRecord(o) || typeof o.const !== "string" || !o.const.trim())) say(`object ${name}: prop "${k}": oneOf is a list of options, each with a const (the stored word), a title and a description`);
143
+ else if (raw.oneOf.some((o) => typeof o.description !== "string" || !o.description.trim())) say(`object ${name}: prop "${k}": every option of oneOf says its description, how the model tells it apart`);
144
+ if (raw.enum != null) say(`object ${name}: prop "${k}" says both enum and oneOf; one or the other`);
145
+ }
146
+ if (raw.type === "array" && !isRecord(raw.items)) say(`object ${name}: prop "${k}" is an array and carries no items: an array says the shape of one element, in the same grammar one level down (README 4c)`);
147
+ }
148
+ return out;
149
+ }
150
+
151
+ /** `from: <function>.<return>` on a prop, against the object's functions: `make` (api/make.yaml) and api/service.yaml's (README 4b). */
152
+ export function fromFindings(name, prop, from, service, file = "props.yaml") {
153
+ const out = [];
154
+ const say = (msg) => out.push({ file, msg, refuses: true });
155
+ const [fn, ret, ...rest] = String(from).split(".");
156
+ if (!fn || !ret || rest.length) {
157
+ say(`object ${name}: prop "${prop}" from "${from}" must read <function>.<return>`);
158
+ return out;
159
+ }
160
+ const api = isRecord(service) ? Object.keys(service) : [];
161
+ if (!api.includes(fn)) {
162
+ say(`object ${name}: prop "${prop}" is from "${fn}", which the object does not declare (make is api/make.yaml, the rest api/service.yaml)`);
163
+ return out;
164
+ }
165
+ const returns = service[fn]?.returns;
166
+ if (returns && typeof returns === "object" && !(ret in returns)) say(`object ${name}: prop "${prop}" is from "${from}", but ${fn} returns no "${ret}"`);
167
+ return out;
168
+ }
169
+
170
+ /**
171
+ * One function of api/service.yaml (README 4b): `calls` in the node's grammar, `returns`.
172
+ * The list's word is the node's: never `steps`. A call needs a name and a node; names are
173
+ * unique; `for` and `when` are strings.
174
+ */
175
+ export function functionFindings(name, fn, raw, file = "api/service.yaml") {
176
+ const out = [];
177
+ const say = (msg, level) => out.push(level ? { file, msg, level } : { file, msg, refuses: true });
178
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return [{ file, msg: `object ${name}: function ${fn} must be a record with calls and returns`, refuses: true }];
179
+ if ("steps" in raw) say(`object ${name}: function ${fn} says steps; the word is calls, as in a node's api/run.yaml (a step is a node on a canvas)`);
180
+ // THE FILE IS THE SWITCH (README 4b, 2026-09-14): api/service.yaml may be called from outside,
181
+ // api/make.yaml is private. Nothing inside a function says who may call it.
182
+ if ("tool" in raw) say(`object ${name}: function ${fn} says tool; the file is the switch: api/service.yaml may be called from outside, api/make.yaml is private (README 4b)`);
183
+ // ITS INPUTS (README 4b): a JSON schema of what a caller passes, the word an Agent's tools use.
184
+ if ("input" in raw && !(isRecord(raw.input) && raw.input.type === "object" && isRecord(raw.input.properties))) say(`object ${name}: function ${fn}: input is a JSON schema of its arguments: type: object, with properties`);
185
+ const list = raw.calls == null ? [] : raw.calls;
186
+ if (!Array.isArray(list)) {
187
+ say(`object ${name}: function ${fn}: calls must be a list`);
188
+ return out;
189
+ }
190
+ const names = new Set();
191
+ list.forEach((c, i) => {
192
+ if (c == null || typeof c !== "object" || typeof c.name !== "string" || !c.name) {
193
+ say(`object ${name}: function ${fn}: call ${i} needs a name`);
194
+ return;
195
+ }
196
+ if (names.has(c.name)) say(`object ${name}: function ${fn}: two calls named "${c.name}"`);
197
+ names.add(c.name);
198
+ if (typeof c.node !== "string") say(`object ${name}: function ${fn}: call "${c.name}" names no node, the only way an object touches the world (README 12.5)`);
199
+ if (c.for != null && typeof c.for !== "string") say(`object ${name}: function ${fn}: call "${c.name}": for must be a reference to a list`);
200
+ if (c.when != null && typeof c.when !== "string") say(`object ${name}: function ${fn}: call "${c.name}": when must be an expression`);
201
+ for (const k of ["credentials", "allowedHosts", "credential"]) if (k in c) say(`object ${name}: function ${fn}: call "${c.name}" carries "${k}"; a credential is never passed, the node declares its type (README 12.4)`);
202
+ });
203
+ return out;
204
+ }
205
+
206
+ /**
207
+ * The service file whole (README 4b): every function a record; `getDetail` the one fixed
208
+ * name; every call's node one the tier can see, when the tier is visible.
209
+ */
210
+ export function serviceFindings(name, service, nodeTypes = null, file = "api/service.yaml") {
211
+ const out = [];
212
+ if (service == null) return out;
213
+ if (!isRecord(service)) return [{ file, msg: `object ${name}: api/service.yaml is a record of functions by name (getDetail, searchDetail)` }];
214
+ for (const [fn, raw] of Object.entries(service)) {
215
+ out.push(...functionFindings(name, fn, raw, file));
216
+ if (!Array.isArray(raw?.calls)) continue;
217
+ for (const c of raw.calls) {
218
+ if (!c || typeof c.node !== "string") continue;
219
+ if (nodeTypes && !nodeTypes.has(c.node)) out.push({ file, msg: `object ${name}: function ${fn}: call "${c.name}" names node "${c.node}", which neither the marketplace nor this org holds. A call runs a node the org has; the node's folder is design/<tier>/nodes/<package>/nodes/${c.node}/` });
220
+ }
221
+ }
222
+ return out;
223
+ }
224
+
225
+ /**
226
+ * api/make.yaml (README 4b, 2026-09-14): the private make function, one function with no name
227
+ * inside it, run only by the pipeline. The public file never declares it: `service` is the
228
+ * api/service.yaml record, so a make function put there is refused.
229
+ */
230
+ export function makeFindings(name, make, service, nodeTypes = null, file = "api/make.yaml") {
231
+ const out = [];
232
+ if (isRecord(service)) for (const reserved of ["make", "makeDetail"]) if (reserved in service) out.push({ file: "api/service.yaml", msg: `object ${name}: api/service.yaml declares ${reserved}; the make function is private: it lives in api/make.yaml, one function with no name inside it (README 4b)`, refuses: true });
233
+ if (make == null) return out;
234
+ if (!isRecord(make)) return [...out, { file, msg: `object ${name}: api/make.yaml is one function: description, calls and returns`, refuses: true }];
235
+ if ("makeDetail" in make) out.push({ file, msg: `object ${name}: api/make.yaml names makeDetail; the file is the function, so its description, calls and returns sit at the top (README 4b)`, refuses: true });
236
+ out.push(...functionFindings(name, "make", make, file));
237
+ if (nodeTypes && Array.isArray(make.calls)) for (const c of make.calls) if (c && typeof c.node === "string" && !nodeTypes.has(c.node)) out.push({ file, msg: `object ${name}: make: call "${c.name}" names node "${c.node}", which neither the marketplace nor this org holds. A call runs a node the org has; the node's folder is design/<tier>/nodes/<package>/nodes/${c.node}/` });
238
+ return out;
239
+ }
240
+
241
+ /**
242
+ * The row keys against the fields, once `extends` is composed: every field named in
243
+ * uniqueBy, embed and search is a prop. `props` is the composed record.
244
+ */
245
+ export function rowKeyFindings(name, envelope, props, file = `${name}.yaml`) {
246
+ const out = [];
247
+ if (!isRecord(envelope) || !isRecord(props)) return out;
248
+ const known = new Set([...Object.keys(props), "universal_id", "object_type", "source_url", "source_id", "app", "meta"]);
249
+ for (const key of ["uniqueBy", "embed", "search"]) {
250
+ const list = envelope[key];
251
+ if (!Array.isArray(list)) continue;
252
+ 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
+ }
254
+ return out;
255
+ }
256
+
257
+ const camel = (s) => s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
258
+
259
+ /** test.yaml: the fakes name the function's calls; every expect is an expression. */
260
+ export function objectTestFindings(name, test, service, file = "test.yaml") {
261
+ const out = [];
262
+ // A TEST IS FOR A FUNCTION THE ENGINE RUNS: an object whose functions call no node (or
263
+ // has none) has nothing to fake and nothing to return; its row is the object, and the
264
+ // model's work is never faked. Only an object with a call to fake is asked for a test.
265
+ const callsNodes = isRecord(service) && Object.values(service).some((f) => Array.isArray(f?.calls) && f.calls.some((c) => typeof c?.node === "string"));
266
+ if (test == null) return callsNodes ? [{ file, level: "warn", msg: `object ${name}: no test.yaml, yet its functions call nodes. An object nobody can run is an object nobody can trust (README 12.2)` }] : [];
267
+ const td = test?.testData;
268
+ if (!isRecord(td)) return [{ file, msg: `object ${name}: test.yaml carries testData: a row, the fakes by call name, the expects` }];
269
+ const handler = td.row?.content?.handler;
270
+ const fnRaw = handler && isRecord(service) ? service[handler] : null;
271
+ if (handler && isRecord(service) && !fnRaw) out.push({ file, msg: `object ${name}: the test row's handler "${handler}" is no function of the object` });
272
+ const callNames = new Set(Array.isArray(fnRaw?.calls) ? fnRaw.calls.map((c) => c?.name) : []);
273
+ if (isRecord(td.fake) && fnRaw) for (const k of Object.keys(td.fake)) if (!callNames.has(k)) out.push({ file, msg: `object ${name}: test fakes "${k}", which is no call of ${handler}. A fake stands in for one call, by its name` });
274
+ if (isRecord(td.expect)) for (const [k, v] of Object.entries(td.expect)) if (typeof v !== "string" || !v.trim()) out.push({ file, msg: `object ${name}: expect "${k}" is an expression over output` });
275
+ return out;
276
+ }
277
+
278
+ // ─────────────────────────────────────────────────────────────────────────────
279
+ // PIPELINES
280
+ // ─────────────────────────────────────────────────────────────────────────────
281
+
282
+ /** What an entry under `each` says about its row; every other key is an object that row becomes. */
283
+ export const PIPELINE_READ_KEYS = new Set(["each", "sourceUrl", "sourceId", "content"]);
284
+ /** Words the 2026-09-15 shape dropped (docs/unoverse/data/pipelines/history.md), refused with a pointer. */
285
+ const DROPPED_MAKE_KEYS = ["partsOf", "by", "under", "when", "from", "any", "index"];
286
+
287
+ /** 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"]);
289
+ const FIELD = /^[A-Za-z_][A-Za-z0-9_]*$/;
290
+
291
+ /**
292
+ * pipeline.yaml whole (docs/unoverse/data/pipelines/README.md layer 1): the face, `collect` as node steps
293
+ * in order, `make` a list of reads, each naming the objects it makes. `row`, `yields` and `from` are gone.
294
+ * Returns findings in the order the loader used to throw them.
295
+ */
296
+ export function pipelineFindings(name, file, fileName = "pipeline.yaml") {
297
+ const out = [];
298
+ const say = (msg) => out.push({ file: fileName, msg });
299
+ const refuse = (msg) => out.push({ file: fileName, msg, refuses: true });
300
+ if (!isRecord(file)) return [{ file: fileName, msg: `pipeline ${name}: pipeline.yaml must be a record`, refuses: true }];
301
+ if (file.type !== "pipeline") refuse(`pipeline ${name}: pipeline.yaml must declare type: pipeline`);
302
+ if (file.name !== name) refuse(`pipeline ${name}: name must equal the folder`);
303
+ if (!KEBAB.test(name)) say(`pipeline ${name}: the name is kebab-case: it is the folder's name and the connector's`);
304
+ for (const k of FACE_KEYS) if (typeof file[k] !== "string" || !file[k].trim()) say(`pipeline ${name}: the face needs "${k}", as a node's node.yaml (docs/unoverse/data/pipelines/README.md layer 1)`);
305
+ if (typeof file.color !== "string" || !/^#[0-9a-fA-F]{6}$/.test(file.color)) say(`pipeline ${name}: color is the accent on the canvas and the Import screen, a hex colour`);
306
+ if (typeof file.logoUrl !== "string" || !file.logoUrl.trim()) say(`pipeline ${name}: logoUrl is the mark on the canvas and the Import screen`);
307
+ for (const k of ["credentials", "allowedHosts", "url", "method"]) if (k in file) say(`pipeline ${name}: "${k}" is not a word here; nodes are the only way a pipeline touches the world (docs/unoverse/data/pipelines/README.md, decided 2026-09-09 5)`);
308
+
309
+ const stepNames = new Set();
310
+ if (!Array.isArray(file.collect) || !file.collect.length) refuse(`pipeline ${name}: collect must list at least one step`);
311
+ else
312
+ file.collect.forEach((s, i) => {
313
+ if (!isRecord(s) || typeof s.name !== "string" || typeof s.node !== "string") {
314
+ refuse(`pipeline ${name}: collect step ${i} needs a name and a node`);
315
+ return;
316
+ }
317
+ if (stepNames.has(s.name)) refuse(`pipeline ${name}: two collect steps named "${s.name}"`);
318
+ // A step reads only what came before it.
319
+ 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
+ stepNames.add(s.name);
321
+ });
322
+
323
+ if ("row" in file || "yields" in file) refuse(`pipeline ${name}: row and yields are gone; a pipeline is collect and make (docs/unoverse/data/pipelines/README.md layer 1)`);
324
+ // `make` LISTS WHAT IS MADE, IN ORDER (docs/unoverse/data/pipelines/README.md, `make`, 2026-09-15;
325
+ // a-document-decides.md, 2026-09-20): every entry is under `each`, and makes objects from each
326
+ // item's read, with their parts, their index and the `where` that says which reads make them.
327
+ // There is no entry made once per folder: a folder is where a file was found, never what it is.
328
+ const see = `docs/unoverse/data/pipelines/README.md, make`;
329
+ if (isRecord(file.make)) refuse(`pipeline ${name}: make is a list: "- each: <list>" and the objects each item's read becomes (${see})`);
330
+ else if (!Array.isArray(file.make) || !file.make.length) refuse(`pipeline ${name}: make must list at least one object`);
331
+ else {
332
+ const made = [];
333
+ file.make.forEach((raw, i) => {
334
+ const at = `make[${i}]`;
335
+ if (!isRecord(raw)) {
336
+ refuse(`pipeline ${name}: ${at} must be a record`);
337
+ return;
338
+ }
339
+ for (const k of DROPPED_MAKE_KEYS) if (k in raw) refuse(`pipeline ${name}: ${at}: ${k} is not a word here since 2026-09-15; parts and the index sit under their object, and an object made once per pack says where: (${see})`);
340
+ const hasEach = "each" in raw;
341
+ const objects = Object.keys(raw).filter((k) => !PIPELINE_READ_KEYS.has(k) && !DROPPED_MAKE_KEYS.includes(k));
342
+ if (!objects.length) refuse(`pipeline ${name}: ${at} names no object it makes`);
343
+ if (hasEach) {
344
+ const list = typeof raw.each === "string" ? raw.each.trim() : "";
345
+ if (!list || !REF_OR_EXPR.test(list)) refuse(`pipeline ${name}: ${at}: each is a list over collect, {{ collect.<step>.<list> }}`);
346
+ if ("content" in raw && !isRecord(raw.content)) refuse(`pipeline ${name}: ${at}: content is what the row carries instead of the item as listed, a record`);
347
+ for (const k of ["sourceUrl", "sourceId"]) if (k in raw && (typeof raw[k] !== "string" || !raw[k].trim())) refuse(`pipeline ${name}: ${at}: ${k} is an expression over item`);
348
+ for (const ref of collectRefs([raw.each, raw.sourceUrl, raw.sourceId, raw.content])) if (!stepNames.has(ref)) say(`pipeline ${name}: ${at} reads collect.${ref}, which is no collect step`);
349
+ } else {
350
+ // A FOLDER IS NOT A THING (docs/unoverse/data/pipelines/a-document-decides.md, 2026-09-20).
351
+ // An entry with no `each` was made once per folder, and its row's identity was the folder's
352
+ // address, so two funds named in one folder were one row and a figure could name a fund
353
+ // with no row to belong to. What an object IS comes from the read that states it.
354
+ refuse(`pipeline ${name}: ${at}: every entry reads a list; an object made once per folder is gone, say where: on the object beside each: (${see})`);
355
+ }
356
+ for (const o of objects) {
357
+ const spec = raw[o];
358
+ if (!isRecord(spec)) {
359
+ refuse(`pipeline ${name}: ${at}.${o}: an object is its name with a record ({ cardinality: one })`);
360
+ continue;
361
+ }
362
+ if (made.includes(o)) refuse(`pipeline ${name}: ${at}.${o} is made above already; one entry makes an object`);
363
+ 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})`);
365
+ // HOW MANY IS SAID by every object the model writes (owner, 2026-09-13).
366
+ 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
+ if ("any" in spec && spec.any !== true) refuse(`pipeline ${name}: ${at}.${o}: any: true is its only value, the item MAY be one`);
368
+ if ("parts" in spec) {
369
+ if (!isRecord(spec.parts) || !Object.keys(spec.parts).length) refuse(`pipeline ${name}: ${at}.${o}: parts names each part and the list of the read it comes from: table: "{{ detail.tables }}"`);
370
+ else for (const [part, from] of Object.entries(spec.parts)) {
371
+ if (typeof from !== "string" || !REF_OR_EXPR.test(from.trim())) refuse(`pipeline ${name}: ${at}.${o}.parts.${part}: a list of what ${o}'s read returned, {{ detail.<list> }}`);
372
+ if (made.includes(part)) refuse(`pipeline ${name}: ${at}.${o}.parts.${part} is made above already`);
373
+ made.push(part);
374
+ }
375
+ }
376
+ if ("where" in spec) {
377
+ if (!isRecord(spec.where) || !Object.keys(spec.where).length) refuse(`pipeline ${name}: ${at}.${o}: where lists, per field, the values the READ must hold for this item to make it: reportType: [offering-memorandum]`);
378
+ else for (const [field, values] of Object.entries(spec.where))
379
+ if (!FIELD.test(field) || !Array.isArray(values) || !values.length || values.some((v) => typeof v !== "string" && typeof v !== "number"))
380
+ refuse(`pipeline ${name}: ${at}.${o}.where.${field}: a field's name and the list of values it may hold`);
381
+ }
382
+ made.push(o);
383
+ }
384
+ });
385
+ }
386
+ return out;
387
+ }
388
+
389
+ /** Every `collect.<step>` a value reads, through `{{ }}` and return expressions alike. */
390
+ export function collectRefs(value) {
391
+ const out = new Set();
392
+ const walk = (v) => {
393
+ if (typeof v === "string") for (const m of v.matchAll(/\bcollect\.([A-Za-z_][A-Za-z0-9_]*)/g)) out.add(m[1]);
394
+ else if (Array.isArray(v)) v.forEach(walk);
395
+ else if (isRecord(v)) Object.values(v).forEach(walk);
396
+ };
397
+ walk(value);
398
+ return out;
399
+ }
400
+
401
+ /** Every `config.<key>` a value reads. */
402
+ export function configRefs(value) {
403
+ const out = new Set();
404
+ const walk = (v) => {
405
+ if (typeof v === "string") for (const m of v.matchAll(/\bconfig\.([A-Za-z_][A-Za-z0-9_]*)/g)) out.add(m[1]);
406
+ else if (Array.isArray(v)) v.forEach(walk);
407
+ else if (isRecord(v)) Object.values(v).forEach(walk);
408
+ };
409
+ walk(value);
410
+ return out;
411
+ }
412
+
413
+ /**
414
+ * The cross-file rules of a pipeline folder: every collect node one the tier can see, every
415
+ * make key an object the tier can see, every config.<key> a property of config.yaml, the
416
+ * test's fakes named for collect steps.
417
+ */
418
+ export function pipelineFolderFindings(name, { file, config, test }, { nodeTypes = null, objectNames = null } = {}) {
419
+ const out = [];
420
+ if (!isRecord(file)) return out;
421
+ const steps = Array.isArray(file.collect) ? file.collect.filter(isRecord) : [];
422
+ if (nodeTypes) for (const s of steps) if (typeof s.node === "string" && !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
+ 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
+ const props = isRecord(config?.configSchema?.properties) ? new Set(Object.keys(config.configSchema.properties)) : null;
425
+ const refs = configRefs(file);
426
+ if (refs.size && !config) out.push({ file: "config.yaml", msg: `pipeline ${name}: reads config.${[...refs].join(", config.")} but has no config.yaml. What a person sets to run it is the Import form, a node's configSchema` });
427
+ else if (props) for (const r of refs) if (!props.has(r)) out.push({ file: "config.yaml", msg: `pipeline ${name}: reads config.${r}, which config.yaml does not declare` });
428
+ if (test == null) out.push({ file: "test.yaml", level: "warn", msg: `pipeline ${name}: no test.yaml. A pipeline nobody can run is a pipeline nobody can trust` });
429
+ else if (isRecord(test?.testData?.fake)) {
430
+ const stepNames = new Set(steps.map((s) => s.name));
431
+ for (const k of Object.keys(test.testData.fake)) if (!stepNames.has(k)) out.push({ file: "test.yaml", msg: `pipeline ${name}: test fakes "${k}", which is no collect step. A fake stands in for one step, by its name` });
432
+ }
433
+ return out;
434
+ }
435
+
436
+ /** The first error's message. */
437
+ export function firstError(findings) {
438
+ const f = findings.find((x) => !x.level || x.level === "error");
439
+ return f ? f.msg : null;
440
+ }
441
+
442
+ /** The first finding a loader cannot read past, for a loader that refuses a file by throwing; null when the file loads. */
443
+ export function firstRefusal(findings) {
444
+ const f = findings.find((x) => x.refuses);
445
+ return f ? f.msg : null;
446
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Types for the prompts linter (Agent skills and prompt blocks), which is plain JavaScript.
3
+ *
4
+ * Same arrangement as ../objects/index.d.mts: the linter stays .mjs so the CLIs run it
5
+ * under bare node, and TypeScript consumers (the publish gate, Studio) get a shape.
6
+ */
7
+
8
+ export interface Finding {
9
+ level: "error" | "warn" | "hint";
10
+ file: string;
11
+ line?: number;
12
+ msg: string;
13
+ }
14
+
15
+ export interface PromptsResult {
16
+ problems: Finding[];
17
+ skillCount: number;
18
+ blockCount: number;
19
+ designRoot: string;
20
+ }
21
+
22
+ export interface LintPromptsOptions {
23
+ /** Absolute path -> text that stands in for what is on disk (an unsaved editor). */
24
+ overlay?: Record<string, string>;
25
+ }
26
+
27
+ /** Lint every skill and prompt block under `designRoot`, every tier. Prints nothing, throws nothing for a lint failure. */
28
+ export function lintPrompts(designRoot: string, options?: LintPromptsOptions): PromptsResult;
29
+
30
+ /** Frontmatter and body of a markdown file, the way the run-time loaders split it. */
31
+ export function splitFrontmatter(text: string): { data: Record<string, unknown> | null; body: string; error: string | null; line: number };
32
+
33
+ /** True when anything would fail a build. */
34
+ export function hasErrors(result: PromptsResult): boolean;