@unoverse-platform/studio 0.1.96 → 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.
- package/bin/studio.mjs +3 -3
- package/local/authoring.ts +1 -1
- package/local/catalog.ts +22 -2
- package/local/keys.ts +1 -1
- package/local/nodes.ts +29 -42
- package/local/plugin.ts +75 -13
- package/local/sample-identity/brand.yaml +24 -0
- package/local/sample-identity/organisation.yaml +14 -0
- package/local/sample-identity/purpose.yaml +14 -0
- package/local/sample-identity/story.yaml +18 -0
- package/local/scaffold.mjs +51 -22
- package/local/source.ts +4 -9
- package/package.json +1 -1
- package/screens/AgentsView.tsx +173 -0
- package/screens/ComponentsView.tsx +2 -2
- package/screens/EmptyLibrary.tsx +1 -1
- package/screens/IdentityHeader.tsx +1 -1
- package/screens/IdentityView.tsx +206 -0
- package/screens/PromptBlocksView.tsx +1 -1
- package/screens/SourceView.tsx +1 -1
- package/screens/TemplatesLibraryView.tsx +91 -6
- package/screens/TemplatesView.tsx +24 -9
- package/screens/index.ts +5 -2
- package/src/App.tsx +14 -4
- package/src/NodeKeys.tsx +1 -1
- package/src/host.ts +3 -16
- package/vendor/base/src/agent-mcp/discovery.ts +76 -18
- package/vendor/base/src/agent-mcp/handoff.ts +11 -31
- package/vendor/base/src/agent-mcp/index.ts +5 -5
- package/vendor/base/src/agent-mcp/invoke.ts +37 -19
- package/vendor/base/src/agent-mcp/lean.ts +19 -0
- package/vendor/base/src/agent-mcp/memoryContext.ts +15 -9
- package/vendor/base/src/agent-mcp/memoryIngest.ts +8 -3
- package/vendor/base/src/agent-mcp/rowComponents.ts +184 -72
- package/vendor/base/src/agent-mcp/tools.ts +136 -7
- package/vendor/base/src/agent-mcp/types.ts +47 -10
- package/vendor/base/src/definitions/load/compile/component.ts +59 -4
- package/vendor/base/src/definitions/load/compile/sections.ts +7 -1
- package/vendor/base/src/definitions/load/compile/template.ts +19 -15
- package/vendor/base/src/definitions/load/expand/inherit.ts +5 -1
- package/vendor/base/src/definitions/load/expand/referenced.ts +21 -2
- package/vendor/base/src/definitions/load/expand/refs.ts +14 -0
- package/vendor/base/src/definitions/load/expand/walk.ts +88 -38
- package/vendor/base/src/definitions/load/load.ts +37 -28
- package/vendor/base/src/definitions/model.ts +8 -12
- package/vendor/base/src/definitions/registry/agents.ts +111 -0
- package/vendor/base/src/definitions/registry/catalog.ts +28 -12
- package/vendor/base/src/definitions/registry/datasetSql.ts +149 -0
- package/vendor/base/src/definitions/registry/homes.ts +69 -5
- package/vendor/base/src/definitions/registry/objects.ts +413 -0
- package/vendor/base/src/definitions/registry/orgDocuments.ts +107 -0
- package/vendor/base/src/definitions/registry/orgPack.ts +50 -0
- package/vendor/base/src/definitions/registry/pipelines.ts +197 -0
- package/vendor/base/src/definitions/theme.ts +1 -2
- package/vendor/base/src/items/collect.ts +33 -25
- package/vendor/base/src/items/publish.ts +31 -7
- package/vendor/base/src/lint/context.mjs +17 -0
- package/vendor/base/src/lint/design/file.mjs +199 -2
- package/vendor/base/src/lint/design/index.mjs +66 -73
- package/vendor/base/src/lint/design/vocabulary.mjs +6 -3
- package/vendor/base/src/lint/design/walk.mjs +14 -4
- package/vendor/base/src/lint/index.mjs +15 -9
- package/vendor/base/src/lint/nodes/blocks.mjs +6 -2
- package/vendor/base/src/lint/nodes/index.mjs +6 -1
- package/vendor/base/src/lint/nodes/node.mjs +157 -7
- package/vendor/base/src/lint/objects/index.d.mts +32 -0
- package/vendor/base/src/lint/objects/index.mjs +324 -0
- package/vendor/base/src/lint/objects/rules.d.mts +44 -0
- package/vendor/base/src/lint/objects/rules.mjs +446 -0
- package/vendor/base/src/lint/prompts/index.d.mts +34 -0
- package/vendor/base/src/lint/prompts/index.mjs +198 -0
- package/vendor/base/src/manifests/compose.ts +81 -10
- package/vendor/base/src/manifests/executor/callback.ts +15 -4
- package/vendor/base/src/manifests/executor/context.ts +12 -1
- package/vendor/base/src/manifests/executor/memory.ts +83 -3
- package/vendor/base/src/manifests/executor/session.ts +50 -0
- package/vendor/base/src/manifests/executor/toolBridge.ts +114 -49
- package/vendor/base/src/manifests/index.ts +1 -1
- package/vendor/base/src/manifests/runtime/auth/aws.ts +14 -1
- package/vendor/base/src/manifests/runtime/channels/final.ts +46 -34
- package/vendor/base/src/manifests/runtime/channels/service.ts +30 -32
- package/vendor/base/src/manifests/runtime/channels/workflow.ts +6 -11
- package/vendor/base/src/manifests/runtime/context.ts +18 -6
- package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +159 -3
- package/vendor/base/src/manifests/runtime/duplex/session.ts +304 -9
- package/vendor/base/src/manifests/runtime/events.ts +39 -3
- package/vendor/base/src/manifests/runtime/http/concurrency.ts +44 -0
- package/vendor/base/src/manifests/runtime/http/request.ts +36 -7
- package/vendor/base/src/manifests/runtime/index.ts +3 -2
- package/vendor/base/src/manifests/runtime/loops/repeat.ts +86 -0
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +30 -3
- package/vendor/base/src/manifests/runtime/usage.ts +10 -0
- package/vendor/base/src/manifests/source.ts +1 -1
- package/vendor/base/src/paths.ts +16 -8
- package/vendor/base/src/platform/audioManager.ts +32 -0
- package/vendor/base/src/platform/executionContext.ts +14 -0
- package/vendor/base/src/platform/liveInput.ts +52 -0
- package/vendor/base/src/plugins/README.md +1 -1
- package/vendor/base/src/plugins/discovery.ts +1 -1
- package/vendor/base/src/template/SafeExpression.ts +6 -0
- package/vendor/base/src/workflow/GetMemoryService.ts +12 -11
- package/vendor/base/src/workflow/PlatformMemoryCore.ts +23 -10
- package/vendor/sdk/appEngine.tsx +31 -12
- package/vendor/sdk/lib/UnoverseComponent.tsx +8 -4
- package/vendor/sdk/lib/app.tsx +70 -95
- package/vendor/sdk/lib/connection.tsx +47 -57
- package/vendor/sdk/lib/core/actions.ts +6 -0
- package/vendor/sdk/lib/core/apps.ts +66 -81
- package/vendor/sdk/lib/core/client.ts +5 -14
- package/vendor/sdk/lib/core/connection.ts +30 -0
- package/vendor/sdk/lib/core/index.ts +2 -2
- package/vendor/sdk/lib/core/store.ts +90 -15
- package/vendor/sdk/lib/core/types.ts +9 -8
- package/vendor/sdk/lib/primitives.tsx +113 -4
- package/vendor/sdk/lib/realtime/useAudioPlayback.ts +8 -1
- package/vendor/sdk/lib/render.tsx +3 -3
- package/vendor/sdk/lib/voice.tsx +27 -13
- package/vite.config.ts +1 -1
|
@@ -67,7 +67,7 @@ export function lintDefinitions(designRoot, options = {}) {
|
|
|
67
67
|
try {
|
|
68
68
|
return (
|
|
69
69
|
statSync(d).isDirectory() &&
|
|
70
|
-
["components", "atoms", "styles", "apps"].some((s) => existsSync(join(d, s)))
|
|
70
|
+
["components", "atoms", "styles", "apps", "agents"].some((s) => existsSync(join(d, s)))
|
|
71
71
|
);
|
|
72
72
|
} catch {
|
|
73
73
|
return false;
|
|
@@ -96,9 +96,7 @@ export function lintDefinitions(designRoot, options = {}) {
|
|
|
96
96
|
const DS = (() => {
|
|
97
97
|
const onDisk = join(DESIGN_ROOT, "marketplace");
|
|
98
98
|
if (existsSync(onDisk)) return onDisk;
|
|
99
|
-
const nodesHome = resolve(DESIGN_ROOT, "..", "nodes");
|
|
100
99
|
for (const c of [
|
|
101
|
-
join(nodesHome, "marketplace", "definitions"),
|
|
102
100
|
join(resolve(DESIGN_ROOT, "..", "plugins"), "node_modules", "@unoverse-platform", "marketplace", "definitions"),
|
|
103
101
|
])
|
|
104
102
|
if (existsSync(c)) return c;
|
|
@@ -190,6 +188,9 @@ const isAppPath = (f) => homeSegments(f).includes("apps");
|
|
|
190
188
|
// carry no manifest and declare no state tree, the face being the filename (§9b rule 7).
|
|
191
189
|
const isTemplatePath = (f) => homeSegments(f).includes("templates");
|
|
192
190
|
const isTemplatePart = (f) => isTemplatePath(f) && homeSegments(f).includes("components");
|
|
191
|
+
// THE AGENT KIND (docs.unoverse.ai/design/tasks, 2026-09-06): one file under <org>/agents/,
|
|
192
|
+
// a workflow listed for the map. Not a UI tree, so the tree rules do not apply; its own check does.
|
|
193
|
+
const isAgentPath = (f) => homeSegments(f).includes("agents");
|
|
193
194
|
// The DEFINITION ROOT folder for a file: a bare partial lives one level under it
|
|
194
195
|
// (layouts/ states/ components/ blocks/); every `$include` resolves against it.
|
|
195
196
|
const defRoot = (file) => {
|
|
@@ -597,7 +598,7 @@ const keyHomes = [
|
|
|
597
598
|
[join(DS, "components"), true],
|
|
598
599
|
];
|
|
599
600
|
for (const orgDir of orgDirs) {
|
|
600
|
-
keyHomes.push([join(orgDir, "components"), true], [join(orgDir, "apps"), true]);
|
|
601
|
+
keyHomes.push([join(orgDir, "components"), true], [join(orgDir, "apps"), true], [join(orgDir, "agents"), false]);
|
|
601
602
|
}
|
|
602
603
|
for (const [dir, isDir] of keyHomes) {
|
|
603
604
|
if (!existsSync(dir)) continue;
|
|
@@ -638,7 +639,9 @@ for (const [dir, isDir] of keyHomes) {
|
|
|
638
639
|
// carries are ordinary components, so they were never exempt from anything.
|
|
639
640
|
const homes = [{ dir: join(DS, "components") }, { dir: join(DS, "atoms") }, { dir: join(DS, "templates") }];
|
|
640
641
|
for (const orgDir of orgDirs) {
|
|
641
|
-
|
|
642
|
+
// `agents` (2026-09-06): a kind is linted; a home not enumerated is a home where every
|
|
643
|
+
// rule is off, silently.
|
|
644
|
+
for (const sub of ["apps", "components", "templates", "agents"]) {
|
|
642
645
|
const d = join(orgDir, sub);
|
|
643
646
|
if (existsSync(d) && statSync(d).isDirectory()) homes.push({ dir: d });
|
|
644
647
|
}
|
|
@@ -701,64 +704,27 @@ for (const orgDir of orgDirs) {
|
|
|
701
704
|
);
|
|
702
705
|
}
|
|
703
706
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
* Declaring it makes whatever wears that face ADDRESSABLE: the pill goes up and the guest's
|
|
708
|
-
* next sentences travel to that instance instead of opening a new answer. Every finding here
|
|
709
|
-
* is something that produces a pill which looks fine and behaves wrongly, which is why they
|
|
710
|
-
* are errors rather than warnings.
|
|
711
|
-
*
|
|
712
|
-
* Read from the ENVELOPE first and the manifest second, the same order the loader reads the
|
|
713
|
-
* states tree in — a rule that judged only one home would pass whatever was authored in the
|
|
714
|
-
* other.
|
|
715
|
-
*/
|
|
716
|
-
const LATCH_KEYS = new Set(["title", "icon"]);
|
|
707
|
+
// ── `latch`, RETIRED (2026-09-07) ──
|
|
708
|
+
// The conversation is never "addressed at" a component: what is on screen is the server's
|
|
709
|
+
// fact and the model reads it. A leftover block is an intent nothing honours, so it errors.
|
|
717
710
|
for (const orgDir of orgDirs) {
|
|
718
711
|
const adir = join(orgDir, "apps");
|
|
719
712
|
if (!(existsSync(adir) && statSync(adir).isDirectory())) continue;
|
|
720
713
|
for (const e of readdirSync(adir)) {
|
|
721
714
|
const folder = join(adir, e);
|
|
722
|
-
let src, states;
|
|
723
715
|
try {
|
|
724
716
|
if (!statSync(folder).isDirectory()) continue;
|
|
725
|
-
|
|
726
|
-
const envelope = defPath(folder, e);
|
|
727
|
-
const manifest = defPath(folder, "manifest");
|
|
728
|
-
for (const p of [envelope, manifest]) {
|
|
717
|
+
for (const p of [defPath(folder, e), defPath(folder, "manifest")]) {
|
|
729
718
|
if (!p || !existsSync(p)) continue;
|
|
730
|
-
const
|
|
731
|
-
if (
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
719
|
+
const states = readDef(p).states;
|
|
720
|
+
if (!states || typeof states !== "object" || Array.isArray(states)) continue;
|
|
721
|
+
for (const [sname, sentry] of Object.entries(states))
|
|
722
|
+
if (sentry && typeof sentry === "object" && !Array.isArray(sentry) && sentry.latch !== undefined)
|
|
723
|
+
report("error", p, `state "${sname}": "latch" is retired (2026-09-07). Delete the block — the model reads what is on screen; nothing addresses a component`);
|
|
736
724
|
}
|
|
737
725
|
} catch {
|
|
738
726
|
continue; // a malformed envelope is reported elsewhere
|
|
739
727
|
}
|
|
740
|
-
if (!states) continue;
|
|
741
|
-
for (const [sname, sentry] of Object.entries(states)) {
|
|
742
|
-
const latch = sentry && typeof sentry === "object" && !Array.isArray(sentry) ? sentry.latch : undefined;
|
|
743
|
-
if (latch === undefined) continue;
|
|
744
|
-
if (typeof latch !== "object" || latch === null || Array.isArray(latch)) {
|
|
745
|
-
report("error", src, `state "${sname}": "latch" must be a block ({ title, icon }), not ${Array.isArray(latch) ? "a list" : typeof latch}. Presence is what makes a state addressable, so a malformed one is an intent nothing can honour (docs/MCP_COMPLETE_GUIDE.md §The Component Latch)`);
|
|
746
|
-
continue;
|
|
747
|
-
}
|
|
748
|
-
// The pill is the ONLY sign the guest has that their words are landing on this page
|
|
749
|
-
// rather than the conversation. Unlabelled, it says nothing — and the compiler drops
|
|
750
|
-
// a titleless block, so the state would silently stop being addressable.
|
|
751
|
-
if (typeof latch.title !== "string" || !latch.title.trim())
|
|
752
|
-
report("error", src, `state "${sname}": "latch" needs a "title" — it is what the pill reads, and the pill is the only thing telling the guest what they are addressing. Name the thing, not the verb ("Your Perfect Day", not "Edit the day")`);
|
|
753
|
-
const extra = Object.keys(latch).filter((k) => !LATCH_KEYS.has(k));
|
|
754
|
-
if (extra.length)
|
|
755
|
-
report("error", src, `state "${sname}": "latch" has no key(s) ${extra.join(", ")}. The block is exactly ${[...LATCH_KEYS].join(", ")} — an invented key is silently ignored, so whatever it was meant to do never happens. The pill's colours belong to the design system's composer-bar, never to a state`);
|
|
756
|
-
// A STATE NOTHING CAN ENTER CANNOT BE ADDRESSED. The base state is the app at rest —
|
|
757
|
-
// the conversation itself — and `stateOrder` excludes it, so `resolveLatch` can never
|
|
758
|
-
// rank a base-state instance and the pill would never appear.
|
|
759
|
-
if (sname === Object.keys(states)[0])
|
|
760
|
-
report("error", src, `state "${sname}": it is the BASE state — the app at rest, which the priority ladder excludes by construction (stateOrder is every state BUT the base). A latch here can never resolve, so no pill would ever draw. Declare it on a state something ARRIVES into`);
|
|
761
|
-
}
|
|
762
728
|
}
|
|
763
729
|
}
|
|
764
730
|
|
|
@@ -778,8 +744,14 @@ const PLATFORM_HANDLERS = new Set(["getDetail"]); // named handlers needing no f
|
|
|
778
744
|
// silently disable every rule after it.
|
|
779
745
|
const credentialDefs = new Set();
|
|
780
746
|
{
|
|
781
|
-
|
|
747
|
+
// Every tier's node packages: the marketplace's and each org's, all at <tier>/nodes/.
|
|
748
|
+
const nodesHomes = [];
|
|
782
749
|
try {
|
|
750
|
+
for (const tier of readdirSync(DESIGN_ROOT)) nodesHomes.push(join(DESIGN_ROOT, tier, "nodes"));
|
|
751
|
+
} catch {
|
|
752
|
+
/* no design root: nothing declares a credential */
|
|
753
|
+
}
|
|
754
|
+
for (const nodesHome of nodesHomes) try {
|
|
783
755
|
for (const pkg of readdirSync(nodesHome)) {
|
|
784
756
|
try {
|
|
785
757
|
for (const f of readdirSync(join(nodesHome, pkg, "credentials"))) if (isDefFile(f)) credentialDefs.add(defName(f));
|
|
@@ -859,24 +831,8 @@ for (const orgDir of [DS, ...orgDirs]) {
|
|
|
859
831
|
if (f.endsWith(".js") && !declared.has(f.slice(0, -3)))
|
|
860
832
|
report("error", join(folder, f), `${f} is a lifecycle handler no manifest opted into. Add "${f.slice(0, -3)}" to the manifest's lifecycle array, or delete the file — un-opted code never runs`);
|
|
861
833
|
|
|
862
|
-
/**
|
|
863
|
-
* LATCH, RETIRED FROM THE MANIFEST (2026-08-31, MCP guide §The Component Latch).
|
|
864
|
-
*
|
|
865
|
-
* It moved to the STATE, in the app's own tree, beside that state's `layout:` and
|
|
866
|
-
* `preview:`. A component said one thing about itself for the whole of its life, but a
|
|
867
|
-
* page is addressable when it IS the page and not when it is a summary in the flow, and
|
|
868
|
-
* only the state knows which of those is on screen.
|
|
869
|
-
*
|
|
870
|
-
* An ERROR rather than a silent ignore: nothing reads this key any more, so a manifest
|
|
871
|
-
* still carrying it declares an intent that never happens, and the author has no way to
|
|
872
|
-
* tell that from a latch that simply does not work.
|
|
873
|
-
*/
|
|
874
834
|
if (readDef(mf).latch !== undefined)
|
|
875
|
-
report(
|
|
876
|
-
"error",
|
|
877
|
-
mf,
|
|
878
|
-
`"latch" is no longer a manifest key — it moved to the STATE (2026-08-31). Declare it on the app state this component is addressable IN, beside that state's layout: \`states: { <state>: { layout: ..., latch: { title: "..." } } }\`. Nothing reads a manifest latch, so this one produces no pill (docs/MCP_COMPLETE_GUIDE.md §The Component Latch)`,
|
|
879
|
-
);
|
|
835
|
+
report("error", mf, `"latch" is retired (2026-09-07). Delete the key — nothing reads it`);
|
|
880
836
|
}
|
|
881
837
|
}
|
|
882
838
|
|
|
@@ -910,9 +866,20 @@ function readAppSizes(dir) {
|
|
|
910
866
|
try {
|
|
911
867
|
const sizes = {};
|
|
912
868
|
for (const [k, v] of Object.entries(readDef(p).appSize ?? {})) sizes[k] = typeof v === "string" ? v : v?.$value;
|
|
869
|
+
// `flex` IS THE SDK'S OWN WORD, NEVER A TOKEN (docs.unoverse.ai/design/apps): it means
|
|
870
|
+
// "fill the space remaining beside whatever core is open", on a slot and on a mounted
|
|
871
|
+
// template alike. A token named flex hard-codes ONE core's width and is wrong beside every
|
|
872
|
+
// other core: observed live 2026-09-10, an org's flex of 100% minus a 360px rail left a
|
|
873
|
+
// mounted grid overlapping a 480px voice core. The word is reserved; the file says so.
|
|
874
|
+
if (Object.prototype.hasOwnProperty.call(sizes, "flex"))
|
|
875
|
+
report("error", p, `app-sizes names "flex". flex is the SDK's own word (fill the space remaining beside whatever core is open), never a token: a token named flex hard-codes one core's width and is wrong beside every other core. Delete it; layouts keep writing appWidth: flex (docs.unoverse.ai/design/apps)`);
|
|
913
876
|
return sizes;
|
|
914
|
-
} catch {
|
|
915
|
-
|
|
877
|
+
} catch (e) {
|
|
878
|
+
// SAID, NOT SWALLOWED (2026-09-10): a sizes file that will not parse used to return null
|
|
879
|
+
// here, and the org silently inherited the foundation's set, so every width of its own
|
|
880
|
+
// "named no app size" and the real fault, a stray colon in the file, was never named.
|
|
881
|
+
report("error", p, `app-sizes is not valid ${p.endsWith(".json") ? "JSON" : "YAML"}: ${String(e?.message ?? e).split("\n")[0]}. Until it parses, none of this org's own sizes exist`);
|
|
882
|
+
return null;
|
|
916
883
|
}
|
|
917
884
|
}
|
|
918
885
|
function appSizesForFile(file) {
|
|
@@ -975,13 +942,39 @@ function componentNamesForFile(file) {
|
|
|
975
942
|
return names;
|
|
976
943
|
}
|
|
977
944
|
|
|
945
|
+
// THE PLACES APPS DECLARE (UNOVERSE_SCREEN.md §2.2), across the whole tree. A component is shared
|
|
946
|
+
// by every app that shows it, so a state's `place:` is checked against every app's places, plus
|
|
947
|
+
// `chat`. Read once per run: every app layout under any `apps/` folder, every `Place` node in it.
|
|
948
|
+
let placeNamesCache;
|
|
949
|
+
function placeNames() {
|
|
950
|
+
if (placeNamesCache !== undefined) return placeNamesCache;
|
|
951
|
+
const names = new Set();
|
|
952
|
+
const walk = (n) => {
|
|
953
|
+
if (Array.isArray(n)) return n.forEach(walk);
|
|
954
|
+
if (!n || typeof n !== "object") return;
|
|
955
|
+
if (n.type === "Place" && typeof n.name === "string") names.add(n.name);
|
|
956
|
+
for (const v of Object.values(n)) walk(v);
|
|
957
|
+
};
|
|
958
|
+
const roots = [DS, ...readdirSync(DESIGN_ROOT).filter((e) => !e.startsWith(".")).map((e) => join(DESIGN_ROOT, e))];
|
|
959
|
+
for (const root of new Set(roots))
|
|
960
|
+
for (const f of jsonFiles(join(root, "apps"))) {
|
|
961
|
+
try {
|
|
962
|
+
walk(parseDef(readText(f), f));
|
|
963
|
+
} catch {
|
|
964
|
+
/* an unreadable file is reported by its own lint; it declares no place here */
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
placeNamesCache = names;
|
|
968
|
+
return names;
|
|
969
|
+
}
|
|
970
|
+
|
|
978
971
|
|
|
979
972
|
// The run context: everything the extracted rule modules close over. Built here so it
|
|
980
973
|
// cannot outlive the run, and passed once rather than threaded as ten parameters.
|
|
981
974
|
const ctx = {
|
|
982
975
|
DESIGN_ROOT, DS, orgDirs, report, spaceSteps, stepList, checkDimension, checkToken, checkCondition,
|
|
983
|
-
appSizesForFile, componentNamesForFile, refResolves, canonicalRef, declaredProps, atomsDirExists,
|
|
984
|
-
isFixture, isHook, isPropsExtract, isManifest, isAppPath, isTemplatePath, isTemplatePart, defRoot, readText,
|
|
976
|
+
appSizesForFile, componentNamesForFile, placeNames, refResolves, canonicalRef, declaredProps, atomsDirExists,
|
|
977
|
+
isFixture, isHook, isPropsExtract, isManifest, isAppPath, isTemplatePath, isTemplatePart, isAgentPath, defRoot, readText,
|
|
985
978
|
};
|
|
986
979
|
const walkNode = makeWalkNode(ctx);
|
|
987
980
|
const lintFile = makeLintFile({ ...ctx, walkNode });
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// ── ground truth (mirrors design/_schema/unoverse.schema.json + server guards) ──
|
|
14
14
|
export const PRIMITIVES = new Set([
|
|
15
15
|
"Box", "Stack", "Row", "Column", "Each", "Switch", "ComponentSlot", "Timeline",
|
|
16
|
-
"Text", "Image", "Button", "Input", "Select", "Markdown", "Skeleton", "Icon", "Ref", "Orb", "Template",
|
|
16
|
+
"Text", "Image", "Button", "Input", "Select", "Markdown", "Skeleton", "Icon", "Ref", "Orb", "Template", "Place",
|
|
17
17
|
]);
|
|
18
18
|
export const CONDITION_KEYS = new Set(["field", "eq", "ne", "in"]);
|
|
19
19
|
|
|
@@ -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", "
|
|
60
|
+
"tagline", "shortDescription", "introParagraph", "callToAction", "primaryImage", "images", "action",
|
|
61
61
|
"bodyCopy", "features", "mainCategory", "section", "needs",
|
|
62
62
|
]);
|
|
63
63
|
|
|
@@ -78,7 +78,7 @@ export const PRIMITIVE_PROPS = {
|
|
|
78
78
|
Column: ["children", "autoScroll", "revealOnStick", "appWidth"],
|
|
79
79
|
Text: ["value"],
|
|
80
80
|
Image: ["src", "alt"],
|
|
81
|
-
Markdown: ["value", "inheritLinkColor"],
|
|
81
|
+
Markdown: ["value", "inheritLinkColor", "citeAction"],
|
|
82
82
|
Icon: ["icon", "name"],
|
|
83
83
|
Skeleton: ["variant"],
|
|
84
84
|
Orb: ["state", "size"],
|
|
@@ -94,6 +94,9 @@ export const PRIMITIVE_PROPS = {
|
|
|
94
94
|
// template's machinery. `name` addresses the manifest's `templates:` entry;
|
|
95
95
|
// `frame` is the dressing drawn around it when occupied.
|
|
96
96
|
Template: ["name", "frame", "template", "director", "appWidth"],
|
|
97
|
+
// A PLACE (UNOVERSE_SCREEN.md §2.2): a named spot the screen shows interfaces into.
|
|
98
|
+
// `holds` is one or many; `frame` and `fallback` work as they do on a ComponentSlot.
|
|
99
|
+
Place: ["name", "holds", "frame", "fallback", "appWidth"],
|
|
97
100
|
Timeline: ["user", "assistant", "userData", "assistantData", "autoScroll"],
|
|
98
101
|
};
|
|
99
102
|
|
|
@@ -25,10 +25,12 @@ const SEARCH_KEYS = ["component", "type", "include", "pick", "items"];
|
|
|
25
25
|
// The `object_type`s a place may admit. Apps and skills are absent by construction: the
|
|
26
26
|
// page's own searches are SILENT (no app unlocked, no tool minted, no card lane, no
|
|
27
27
|
// director woken), or every place would rain cards onto a page still assembling itself.
|
|
28
|
-
|
|
28
|
+
// `products` since migration 035 (2026-09-05; `services` retired 2026-09-07, `actions` with it:
|
|
29
|
+
// no row is written as one). Each word maps to a declared spatial type (guarded by a test).
|
|
30
|
+
const QUERY_TYPES = ["products", "images", "needs"];
|
|
29
31
|
|
|
30
32
|
export function makeWalkNode(ctx) {
|
|
31
|
-
const { report, checkCondition, checkDimension, checkToken, appSizesForFile, componentNamesForFile, refResolves, canonicalRef, declaredProps, atomsDirExists, stepList, spaceSteps, isAppPath, isTemplatePath, defRoot } = ctx;
|
|
33
|
+
const { report, checkCondition, checkDimension, checkToken, appSizesForFile, componentNamesForFile, refResolves, canonicalRef, declaredProps, atomsDirExists, stepList, spaceSteps, isAppPath, isTemplatePath, isTemplatePart, defRoot } = ctx;
|
|
32
34
|
// One director per LAYOUT FILE, tracked for this run only (each file is walked once).
|
|
33
35
|
const directorsSeen = new Map();
|
|
34
36
|
|
|
@@ -36,6 +38,13 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
|
|
|
36
38
|
if (Array.isArray(node)) return node.forEach((n) => walkNode(n, file, root, widthCap));
|
|
37
39
|
if (!node || typeof node !== "object") return;
|
|
38
40
|
|
|
41
|
+
// THE ✕ BELONGS TO THE TEMPLATE'S LAYOUT, NEVER TO A PART (2026-09-07). `setValue` writes
|
|
42
|
+
// the interface's OWN slice (docs/design/state.md) and a part is its own interface, so a
|
|
43
|
+
// close inside a part retracts the PART into the chat column while the page stays open
|
|
44
|
+
// beside it (observed live: a page's hero, ✕ and all, sitting in the flow).
|
|
45
|
+
if (node.type === "Ref" && node.ref === "close-button" && isTemplatePart(file))
|
|
46
|
+
report("error", file, `a close-button inside a template PART writes the part's own slice, not the template's — the page stays open and the part falls into the chat. Move the ✕ to the template's layout (layouts/<state>.yaml), where its setValue writes the template's state`);
|
|
47
|
+
|
|
39
48
|
// $include — resolves against the DEFINITION ROOT (layouts/ states/ components/…)
|
|
40
49
|
if (typeof node.$include === "string") {
|
|
41
50
|
const a = defPath(root, node.$include);
|
|
@@ -129,8 +138,9 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
|
|
|
129
138
|
report("error", file, `a delivery band has no "${k}". It reads ${DELIVERY_KEYS.map((x) => `"${x}"`).join(", ")} (§9b; a place that names a component is a search place)`);
|
|
130
139
|
if (d.pick !== undefined && (typeof d.pick !== "number" || d.pick < 1))
|
|
131
140
|
report("error", file, `director.pick is a NUMBER — how many the director keeps, a cap the machine enforces (§9e rule 9)`);
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
// NOTHING READS `rules`: the director's model was removed (UNOVERSE_SCREEN.md §2.7).
|
|
142
|
+
if (d.rules !== undefined)
|
|
143
|
+
report("warn", file, `director.rules is ignored: no model reads it any more. A band holds what arrives in search rank, up to pick. Remove it`);
|
|
134
144
|
if (d.preview !== undefined && (!Array.isArray(d.preview) || d.preview.some((c) => typeof c !== "string")))
|
|
135
145
|
report("error", file, `director.preview is an array of component names — Studio's stand-ins until a real run exists (§9b rule 6)`);
|
|
136
146
|
else if (Array.isArray(d.preview)) {
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The linters.
|
|
3
|
-
*
|
|
2
|
+
* The linters. FOUR siblings, one per kind of thing a developer authors, and every kind
|
|
3
|
+
* is checked the same way: `unoverse lint`, the publish gate, CI, all through
|
|
4
|
+
* items/publish lintForPublish.
|
|
4
5
|
*
|
|
5
|
-
* nodes/
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* design/
|
|
9
|
-
*
|
|
6
|
+
* nodes/ node manifests: JSON Schema per part, plus the cross-file rules a schema
|
|
7
|
+
* cannot express (events matching output connectors, calls reachable by name,
|
|
8
|
+
* credentials declared in the package that needs them)
|
|
9
|
+
* design/ definitions: a CLOSED primitive set, a closed style vocabulary, token-only
|
|
10
|
+
* values, Switch discriminants, layouts reachable from a case
|
|
11
|
+
* objects/ objects and pipelines: schema per file, plus the rules a schema cannot say
|
|
12
|
+
* prompts/ Agent skills and prompt blocks: frontmatter that parses, the fields the
|
|
13
|
+
* loaders need, and the discoverability contract on whenToUse
|
|
10
14
|
*
|
|
11
|
-
* They share `context.mjs` and nothing else. One reports on `api/run.yaml`,
|
|
12
|
-
* whether a colour is a raw hex. Merging them would produce a linter that is worse at
|
|
15
|
+
* They share `context.mjs` and nothing else. One reports on `api/run.yaml`, another on
|
|
16
|
+
* whether a colour is a raw hex. Merging them would produce a linter that is worse at all.
|
|
13
17
|
*
|
|
14
18
|
* WHY THEY LIVE HERE rather than in scripts/. The same rules are needed by four callers,
|
|
15
19
|
* and only one of them is a terminal:
|
|
@@ -25,4 +29,6 @@
|
|
|
25
29
|
|
|
26
30
|
export { lintNodes, hasErrors as nodesHaveErrors } from "./nodes/index.mjs";
|
|
27
31
|
export { lintDefinitions, hasErrors as definitionsHaveErrors } from "./design/index.mjs";
|
|
32
|
+
export { lintObjects, hasErrors as objectsHaveErrors } from "./objects/index.mjs";
|
|
33
|
+
export { lintPrompts, hasErrors as promptsHaveErrors } from "./prompts/index.mjs";
|
|
28
34
|
export { problems, report, rel, reset } from "./context.mjs";
|
|
@@ -23,7 +23,7 @@ export const HANDLEBARS_HELPERS = new Set([
|
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* The prompt-block library:
|
|
26
|
+
* The prompt-block library: design/marketplace/blocks/**\/*.md, keyed the way the resolver keys them
|
|
27
27
|
* (engine/src/template/promptBlocks.ts) — the filename camelCased, so markdown-guidelines.md is
|
|
28
28
|
* referenced as {{prompt.markdownGuidelines}}.
|
|
29
29
|
*
|
|
@@ -41,7 +41,11 @@ export const HANDLEBARS_HELPERS = new Set([
|
|
|
41
41
|
const CACHE = new Map();
|
|
42
42
|
|
|
43
43
|
export function promptBlocks() {
|
|
44
|
-
|
|
44
|
+
// The block library is the marketplace tier's. Beside the nodes when they ARE that
|
|
45
|
+
// tier (design/marketplace/{nodes,blocks}); one tier over for an org's own nodes
|
|
46
|
+
// (design/<org>/nodes → design/marketplace/blocks).
|
|
47
|
+
const home = [resolve(state.nodesHome, "../blocks"), resolve(state.nodesHome, "../../marketplace/blocks")]
|
|
48
|
+
.find((d) => existsSync(d)) ?? resolve(state.nodesHome, "../../marketplace/blocks");
|
|
45
49
|
const cached = CACHE.get(home);
|
|
46
50
|
if (cached) return cached;
|
|
47
51
|
|
|
@@ -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,6 +25,7 @@ import { fileURLToPath } from "node:url";
|
|
|
25
25
|
import { problems, derivedKinds, reset, report } from "../context.mjs";
|
|
26
26
|
import { loadSchemas } from "./schema.mjs";
|
|
27
27
|
import { lintPackage } from "./package.mjs";
|
|
28
|
+
import { lintAttachments } from "./node.mjs";
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Lint every node package under `nodesHome`.
|
|
@@ -72,6 +73,10 @@ export function lintNodes(nodesHome) {
|
|
|
72
73
|
nodeCount += lintPackage(dir);
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
// AFTER every package: an attachment names no host, so whether it fits one cannot be known
|
|
77
|
+
// until every node's configSchema has been read. See lintAttachments.
|
|
78
|
+
lintAttachments();
|
|
79
|
+
|
|
75
80
|
const rank = { error: 0, warn: 1, hint: 2 };
|
|
76
81
|
const sorted = [...problems].sort((a, b) => rank[a.level] - rank[b.level] || a.file.localeCompare(b.file));
|
|
77
82
|
return { problems: sorted, derivedKinds: new Map(derivedKinds), nodeCount, pkgCount, nodesHome: home };
|