@unoverse-platform/studio 0.1.95 → 0.1.97
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 +1 -1
- package/local/authoring.ts +1 -1
- package/local/catalog.ts +17 -1
- package/local/keys.ts +1 -1
- package/local/nodes.ts +1 -1
- package/local/plugin.ts +6 -2
- package/local/sample-identity/brand/brand.yaml +88 -0
- package/local/sample-identity/brand/manifest.yaml +13 -0
- package/local/sample-identity/organisation/manifest.yaml +15 -0
- package/local/sample-identity/organisation/organisation.yaml +53 -0
- package/local/sample-identity/purpose/manifest.yaml +12 -0
- package/local/sample-identity/purpose/purpose.yaml +46 -0
- package/local/sample-identity/story/manifest.yaml +12 -0
- package/local/sample-identity/story/story.yaml +63 -0
- package/local/scaffold.mjs +44 -3
- package/local/source.ts +1 -1
- package/package.json +1 -1
- package/screens/IdentityHeader.tsx +1 -1
- package/screens/IdentityView.tsx +206 -0
- package/screens/SourceView.tsx +1 -1
- package/screens/index.ts +3 -2
- package/src/App.tsx +7 -2
- package/src/NodeKeys.tsx +1 -1
- package/src/host.ts +3 -3
- package/vendor/base/src/agent-mcp/memoryIngest.ts +1 -1
- package/vendor/base/src/definitions/registry/identitySeed.ts +17 -0
- package/vendor/base/src/definitions/registry/orgDocuments.ts +67 -0
- package/vendor/base/src/definitions/registry/orgPack.ts +50 -0
- package/vendor/base/src/items/collect.ts +6 -2
- package/vendor/base/src/items/publish.ts +1 -1
- package/vendor/base/src/lint/design/vocabulary.mjs +1 -1
- package/vendor/base/src/lint/nodes/index.mjs +1 -1
- package/vendor/base/src/lint/nodes/schema.mjs +7 -7
- package/vendor/base/src/manifests/compose.ts +1 -1
- package/vendor/base/src/manifests/executor/context.ts +3 -0
- package/vendor/base/src/manifests/index.ts +1 -1
- package/vendor/base/src/manifests/runtime/context.ts +1 -1
- package/vendor/base/src/manifests/source.ts +1 -1
- package/vendor/base/src/plugins/README.md +1 -1
- package/vendor/sdk/appEngine.tsx +1 -1
- package/vite.config.ts +1 -1
package/bin/studio.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* to avoid. So the tool starts the same dev server `npm run dev` does.
|
|
15
15
|
*
|
|
16
16
|
* No database, no Redis, no engine, no Docker, and no account: those belong to running a
|
|
17
|
-
* universe, which is a different job. See docs/architecture/LOCAL_STUDIO.md.
|
|
17
|
+
* universe, which is a different job. See docs/architecture/authoring/LOCAL_STUDIO.md.
|
|
18
18
|
*/
|
|
19
19
|
import { createServer } from "vite";
|
|
20
20
|
import { dirname, resolve, join, basename } from "path";
|
package/local/authoring.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* completion list is not a hint, it is the whole legal set, and it knows the developer's
|
|
17
17
|
* real tokens in a way a JSON Schema in a text editor cannot.
|
|
18
18
|
*
|
|
19
|
-
* See docs/architecture/LOCAL_STUDIO.md §Authoring in Studio.
|
|
19
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md §Authoring in Studio.
|
|
20
20
|
*/
|
|
21
21
|
import { designHome } from "./design-home.mjs";
|
|
22
22
|
import { resolve } from "path";
|
package/local/catalog.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* the exact problem this migration exists to fix). It imports the SAME modules and sets
|
|
9
9
|
* RX_PATH to the developer's folder before they load.
|
|
10
10
|
*
|
|
11
|
-
* See docs/architecture/LOCAL_STUDIO.md.
|
|
11
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md.
|
|
12
12
|
*/
|
|
13
13
|
import { hasDesignHome } from "./design-home.mjs";
|
|
14
14
|
import { resolve } from "path";
|
|
@@ -24,6 +24,8 @@ type Loaders = {
|
|
|
24
24
|
inputPropKeys: (props: unknown) => string[];
|
|
25
25
|
themeOrgs: () => string[];
|
|
26
26
|
listThemeNames: (org: string) => string[];
|
|
27
|
+
/** The identity documents of one org or every org: `<org>/<doc>` items with manifest and briefed props. */
|
|
28
|
+
listIdentity: (org?: string) => unknown[];
|
|
27
29
|
resolveTheme: (name: string) => unknown;
|
|
28
30
|
DEFAULT_ORG: string;
|
|
29
31
|
/** A project's on-disk home. The writable set is built from these (see source.ts). */
|
|
@@ -94,6 +96,8 @@ export async function loadCatalog(
|
|
|
94
96
|
const defs = await load(resolve(baseSrc, "definitions/definitions.ts"));
|
|
95
97
|
const theme = await load(resolve(baseSrc, "definitions/theme.ts"));
|
|
96
98
|
const inputs = await load(resolve(baseSrc, "definitions/inputs.ts"));
|
|
99
|
+
const homes = await load(resolve(baseSrc, "definitions/registry/homes.ts"));
|
|
100
|
+
const identity = await load(resolve(baseSrc, "definitions/registry/orgDocuments.ts"));
|
|
97
101
|
|
|
98
102
|
return {
|
|
99
103
|
listDefinitions: defs.listDefinitions as Loaders["listDefinitions"],
|
|
@@ -104,6 +108,18 @@ export async function loadCatalog(
|
|
|
104
108
|
inputPropKeys: inputs.inputPropKeys as Loaders["inputPropKeys"],
|
|
105
109
|
themeOrgs: theme.themeOrgs as Loaders["themeOrgs"],
|
|
106
110
|
listThemeNames: theme.listThemeNames as Loaders["listThemeNames"],
|
|
111
|
+
listIdentity: ((org?: string) => {
|
|
112
|
+
const orgs: string[] = org && org !== "all" ? [org] : (homes.listOrgs as () => string[])();
|
|
113
|
+
const out: unknown[] = [];
|
|
114
|
+
for (const o of orgs) {
|
|
115
|
+
const root = (homes.projectDir as (n: string) => string)(o);
|
|
116
|
+
for (const doc of identity.ORG_DOCUMENTS as readonly string[]) {
|
|
117
|
+
const read = (identity.readOrgDocument as (r: string, d: string) => { manifest: unknown; props: unknown; path: string } | null)(root, doc);
|
|
118
|
+
if (read) out.push({ name: `${o}/${doc}`, org: o, doc, manifest: read.manifest, props: read.props, path: read.path });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}) as Loaders["listIdentity"],
|
|
107
123
|
resolveTheme: theme.resolveTheme as Loaders["resolveTheme"],
|
|
108
124
|
DEFAULT_ORG: theme.DEFAULT_ORG as string,
|
|
109
125
|
projectDir: defs.projectDir as Loaders["projectDir"],
|
package/local/keys.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* snake-cased, so `airtableCredential.personalAccessToken` reads from
|
|
14
14
|
* `AIRTABLE_PERSONAL_ACCESS_TOKEN`.
|
|
15
15
|
*
|
|
16
|
-
* See docs/architecture/LOCAL_STUDIO.md.
|
|
16
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md.
|
|
17
17
|
*/
|
|
18
18
|
import { resolve } from "path";
|
|
19
19
|
import { existsSync, readFileSync } from "fs";
|
package/local/nodes.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* The bootstrap comment is explicit that this path touches neither Redis nor the
|
|
15
15
|
* workflow service, which is what makes it usable with no backend.
|
|
16
16
|
*
|
|
17
|
-
* See docs/architecture/LOCAL_STUDIO.md.
|
|
17
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md.
|
|
18
18
|
*/
|
|
19
19
|
import { resolve } from "path";
|
|
20
20
|
import { existsSync } from "fs";
|
package/local/plugin.ts
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Routes mirror the server's shapes exactly, because the screens parse those shapes:
|
|
10
10
|
* GET /dev/orgs GET /dev/themes?org= GET /dev/components?org=
|
|
11
|
-
* GET /dev/apps?org= GET /dev/atoms
|
|
11
|
+
* GET /dev/apps?org= GET /dev/atoms GET /dev/identity?org=
|
|
12
12
|
* GET /local/theme?ref= GET /local/definition?uri=
|
|
13
13
|
* GET /skills /skills/:name GET /prompt-blocks
|
|
14
14
|
* GET|POST /registry/state (no database locally, see below)
|
|
15
15
|
*
|
|
16
|
-
* See docs/architecture/LOCAL_STUDIO.md.
|
|
16
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md.
|
|
17
17
|
*/
|
|
18
18
|
import { designHome } from "./design-home.mjs";
|
|
19
19
|
import { createRequire } from "node:module";
|
|
@@ -360,6 +360,10 @@ export function localStudioHost(): Plugin {
|
|
|
360
360
|
case path === "/dev/apps":
|
|
361
361
|
return json(res, { apps: c.listAppsForWorkbench(org) });
|
|
362
362
|
|
|
363
|
+
// The org's identity documents (design/<org>/identity/<doc>/), one org or all.
|
|
364
|
+
case path === "/dev/identity":
|
|
365
|
+
return json(res, { items: c.listIdentity(org) });
|
|
366
|
+
|
|
363
367
|
case path === "/dev/atoms":
|
|
364
368
|
return json(res, { atoms: c.listDefinitions("atom") });
|
|
365
369
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
unoverse: "1.0"
|
|
2
|
+
# THE BRAND, declared the way a component declares its props: each description is the brief
|
|
3
|
+
# the writer follows; the defaults are this org's own words, as they stand.
|
|
4
|
+
type: identity
|
|
5
|
+
name: brand
|
|
6
|
+
category: Identity
|
|
7
|
+
props:
|
|
8
|
+
name:
|
|
9
|
+
type: string
|
|
10
|
+
input: true
|
|
11
|
+
description: >-
|
|
12
|
+
The brand name as the enterprise presents it, which may differ from its trading name
|
|
13
|
+
(a bank's card brand, a group's consumer brand). The site's logo text or title, not
|
|
14
|
+
the legal entity.
|
|
15
|
+
maxLength: 80
|
|
16
|
+
default: ""
|
|
17
|
+
standsFor:
|
|
18
|
+
type: string
|
|
19
|
+
input: true
|
|
20
|
+
description: >-
|
|
21
|
+
What the name stands for, in one sentence, in the enterprise's own words where it says
|
|
22
|
+
so (a tagline, a strapline, the meaning of the name, a 'who we are' line). Empty when
|
|
23
|
+
it does not say.
|
|
24
|
+
maxLength: 300
|
|
25
|
+
default: ""
|
|
26
|
+
promise:
|
|
27
|
+
type: string
|
|
28
|
+
input: true
|
|
29
|
+
description: >-
|
|
30
|
+
The expectation the brand sets: what people can expect from it, in one or two
|
|
31
|
+
sentences, drawn from the promises the site repeats on its home, about and brand
|
|
32
|
+
pages. Written to the customer.
|
|
33
|
+
maxLength: 400
|
|
34
|
+
default: ""
|
|
35
|
+
represents:
|
|
36
|
+
type: string
|
|
37
|
+
input: true
|
|
38
|
+
description: >-
|
|
39
|
+
How the brand carries the purpose (read purpose.yaml first): in one paragraph, how the
|
|
40
|
+
name, the promise and the way it presents itself express why the enterprise exists. In
|
|
41
|
+
the enterprise's own words where it connects the two; empty when it does not.
|
|
42
|
+
maxLength: 500
|
|
43
|
+
default: ""
|
|
44
|
+
audience:
|
|
45
|
+
type: string
|
|
46
|
+
input: true
|
|
47
|
+
description: >-
|
|
48
|
+
Whom the brand speaks to and how it wants to be seen by them, in one or two sentences,
|
|
49
|
+
from the way the site addresses its customers and describes its position. Never
|
|
50
|
+
invented segments.
|
|
51
|
+
maxLength: 400
|
|
52
|
+
default: ""
|
|
53
|
+
differentiators:
|
|
54
|
+
type: array
|
|
55
|
+
input: true
|
|
56
|
+
description: >-
|
|
57
|
+
What the enterprise claims sets its brand apart, three to six items, each in its own
|
|
58
|
+
words (a first, a leadership claim, a partnership, a heritage). Only claims it makes
|
|
59
|
+
itself.
|
|
60
|
+
maxItems: 6
|
|
61
|
+
default: []
|
|
62
|
+
brands:
|
|
63
|
+
type: array
|
|
64
|
+
input: true
|
|
65
|
+
description: >-
|
|
66
|
+
The brand tree: the sub-brands and product brands the enterprise presents under this
|
|
67
|
+
brand (a card family, an app, a loyalty programme, a subsidiary), one entry per brand
|
|
68
|
+
with its name as written and one sentence on what it is for, nested in `brands` where
|
|
69
|
+
the enterprise nests them. Empty when it presents none; never invented.
|
|
70
|
+
maxItems: 20
|
|
71
|
+
default: []
|
|
72
|
+
rules:
|
|
73
|
+
type: array
|
|
74
|
+
input: true
|
|
75
|
+
description: >-
|
|
76
|
+
Three to six rules the site's copy visibly follows, each as 'Do this' or 'Never that':
|
|
77
|
+
the person it addresses (you, we), its register (plain, formal, warm), sentence
|
|
78
|
+
length, what it capitalises, what it never says.
|
|
79
|
+
maxItems: 6
|
|
80
|
+
default: []
|
|
81
|
+
examples:
|
|
82
|
+
type: array
|
|
83
|
+
input: true
|
|
84
|
+
description: >-
|
|
85
|
+
Three to five sentences copied verbatim from the site that show the voice. Quotes,
|
|
86
|
+
never paraphrase.
|
|
87
|
+
maxItems: 5
|
|
88
|
+
default: []
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# The Brand (docs/edgy/elements/brand.md): our name and what it stands for. The written half;
|
|
2
|
+
# styles/ is the visual half. Published, it is one row on the org's map, found by meaning.
|
|
3
|
+
title: Brand
|
|
4
|
+
description: >-
|
|
5
|
+
The name and what it stands for: the promise it makes, whom it speaks to, what sets it
|
|
6
|
+
apart, the brands inside it, and how it sounds.
|
|
7
|
+
whenToUse: >-
|
|
8
|
+
Answer what the brand stands for, what to expect from it, whom it is for and what sets it
|
|
9
|
+
apart; name the brands inside it; and give any writing its voice, the rules it follows and
|
|
10
|
+
sentences in its own words. Not for what the organisation is or does, that is the
|
|
11
|
+
Organisation, and not for why it exists, that is the Purpose.
|
|
12
|
+
category: Identity
|
|
13
|
+
version: 1.0.0
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# The Organisation (docs/edgy/elements/organisation.md): the one that owns this spatial. This
|
|
2
|
+
# manifest says what the document is for; organisation.yaml is the document. It never sits
|
|
3
|
+
# on the map: it is the scope on every row and the grounding every extraction and every
|
|
4
|
+
# agent wakes with.
|
|
5
|
+
title: Organisation
|
|
6
|
+
description: >-
|
|
7
|
+
Who this organisation is and what it does, in its own words, and the words it uses for its
|
|
8
|
+
products, its customers and itself.
|
|
9
|
+
whenToUse: >-
|
|
10
|
+
Ground any extraction, any agent or any piece of writing in who the organisation is and
|
|
11
|
+
how it speaks. Read whole at wake, never searched for. Not for what it offers, that is the
|
|
12
|
+
products on the map, and not for why it exists or where it came from, those are Purpose
|
|
13
|
+
and Story.
|
|
14
|
+
category: Identity
|
|
15
|
+
version: 1.0.0
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
unoverse: "1.0"
|
|
2
|
+
# THE ORGANISATION, declared the way a component declares its props. Each prop's description
|
|
3
|
+
# is the brief: direction to the writer that fills it, including what not to do. The
|
|
4
|
+
# platform compiles briefs into a tool schema, so an Agent fills this from research the way
|
|
5
|
+
# a copywriter fills an email, and a person reads the same words as guidance. The defaults
|
|
6
|
+
# are this org's own words, as they stand.
|
|
7
|
+
type: identity
|
|
8
|
+
name: organisation
|
|
9
|
+
category: Identity
|
|
10
|
+
props:
|
|
11
|
+
name:
|
|
12
|
+
type: string
|
|
13
|
+
input: true
|
|
14
|
+
description: >-
|
|
15
|
+
The org's name as it trades, exactly as its website writes it in its own header or
|
|
16
|
+
footer. Never a legal suffix unless the site uses one.
|
|
17
|
+
maxLength: 80
|
|
18
|
+
default: ""
|
|
19
|
+
what:
|
|
20
|
+
type: string
|
|
21
|
+
input: true
|
|
22
|
+
description: >-
|
|
23
|
+
One paragraph, 60 to 120 words, in the third person: what the business is, what it
|
|
24
|
+
does, whom it serves, where. Taken from the site's own about, home and footer copy.
|
|
25
|
+
This paragraph grounds every extraction, so it must name the business's actual lines
|
|
26
|
+
of work, not adjectives. Never a slogan, never an industry generality.
|
|
27
|
+
maxLength: 900
|
|
28
|
+
default: ""
|
|
29
|
+
units:
|
|
30
|
+
type: array
|
|
31
|
+
input: true
|
|
32
|
+
description: >-
|
|
33
|
+
The organisation map: every unit the organisation names in its own publications, the
|
|
34
|
+
ones that face customers (divisions, lines of business, regions) and the ones that run
|
|
35
|
+
the organisation (operations, technology, people and HR, finance, risk, legal,
|
|
36
|
+
marketing, and the rest). Sources: the about and careers pages, the annual report's
|
|
37
|
+
organisation or leadership section, a published org chart. One entry per unit with its
|
|
38
|
+
name as the organisation writes it and one sentence on what it does or serves, nested
|
|
39
|
+
in `units` where the organisation nests them. Empty when the sources name none; never
|
|
40
|
+
invented.
|
|
41
|
+
maxItems: 40
|
|
42
|
+
default: []
|
|
43
|
+
vocabulary:
|
|
44
|
+
type: string
|
|
45
|
+
input: true
|
|
46
|
+
description: >-
|
|
47
|
+
The words the business uses, as one paragraph separated by semicolons: its product and
|
|
48
|
+
service names as it spells them, its programme and membership names, the terms it uses
|
|
49
|
+
for its customers, and anything it capitalises. Every name is copied from the site,
|
|
50
|
+
never paraphrased. This is the domain prompt every extraction reads, so completeness
|
|
51
|
+
beats elegance.
|
|
52
|
+
maxLength: 2000
|
|
53
|
+
default: ""
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# The Purpose (docs/edgy/elements/purpose.md): why the enterprise exists and the goals it pursues.
|
|
2
|
+
# Published, it is one row on the org's map, found by meaning.
|
|
3
|
+
title: Purpose
|
|
4
|
+
description: >-
|
|
5
|
+
Why the organisation exists: what its people believe in, what it aims to achieve, and the
|
|
6
|
+
goals it is working toward.
|
|
7
|
+
whenToUse: >-
|
|
8
|
+
Answer why the organisation exists, what its people believe in, what it aims to achieve
|
|
9
|
+
and the goals it is working toward. Read before the Brand, which represents it. Not for
|
|
10
|
+
its history or its scale, that is the Story, and not for its products.
|
|
11
|
+
category: Identity
|
|
12
|
+
version: 1.0.0
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
unoverse: "1.0"
|
|
2
|
+
# THE PURPOSE, declared the way a component declares its props: each description is the brief
|
|
3
|
+
# the writer follows; the defaults are this org's own words, as they stand.
|
|
4
|
+
type: identity
|
|
5
|
+
name: purpose
|
|
6
|
+
category: Identity
|
|
7
|
+
props:
|
|
8
|
+
statement:
|
|
9
|
+
type: string
|
|
10
|
+
input: true
|
|
11
|
+
description: >-
|
|
12
|
+
Why the enterprise exists, in its own words: the purpose or mission statement it
|
|
13
|
+
publishes, quoted where one exists. When it publishes none, one sentence composed only
|
|
14
|
+
from what its about page says it is for. Never a marketing slogan.
|
|
15
|
+
maxLength: 400
|
|
16
|
+
default: ""
|
|
17
|
+
values:
|
|
18
|
+
type: array
|
|
19
|
+
input: true
|
|
20
|
+
description: >-
|
|
21
|
+
What the people of the enterprise believe in and consider valuable: the values it
|
|
22
|
+
publishes, each with its name as the enterprise writes it and the one sentence it
|
|
23
|
+
gives for it. From the values, culture or about pages. Empty when none are published;
|
|
24
|
+
never invented.
|
|
25
|
+
maxItems: 8
|
|
26
|
+
default: []
|
|
27
|
+
ambition:
|
|
28
|
+
type: string
|
|
29
|
+
input: true
|
|
30
|
+
description: >-
|
|
31
|
+
The far end of the purpose: what the enterprise says it aims to achieve for its
|
|
32
|
+
customers, its country or society, in one paragraph, quoted where a vision or ambition
|
|
33
|
+
statement exists, with the horizon it names. Not the goals below; the outcome they
|
|
34
|
+
serve.
|
|
35
|
+
maxLength: 700
|
|
36
|
+
default: ""
|
|
37
|
+
goals:
|
|
38
|
+
type: array
|
|
39
|
+
input: true
|
|
40
|
+
description: >-
|
|
41
|
+
The goal tree: the strategic goals, pillars or commitments the enterprise names (a
|
|
42
|
+
strategy page, a strategy section of the annual report, 'our commitments'). One entry
|
|
43
|
+
per goal with its name as the enterprise writes it and one sentence on why, nested in
|
|
44
|
+
`goals` where it nests them. Empty when none are named; never invented.
|
|
45
|
+
maxItems: 12
|
|
46
|
+
default: []
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# The Story (docs/edgy/elements/story.md): the way the enterprise makes sense of itself.
|
|
2
|
+
# History, present, vision. Published, it is one row on the org's map, found by meaning.
|
|
3
|
+
title: Story
|
|
4
|
+
description: >-
|
|
5
|
+
Where the organisation came from, what it is now and where it is going, and what that
|
|
6
|
+
story says it is for.
|
|
7
|
+
whenToUse: >-
|
|
8
|
+
Answer when and how the organisation began, its milestones, what it is today (its scale,
|
|
9
|
+
its lines of business), where it says it is going, and whom its story is about. Read with
|
|
10
|
+
the Purpose, which it puts in context. Not for why it exists, that is the Purpose.
|
|
11
|
+
category: Identity
|
|
12
|
+
version: 1.0.0
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
unoverse: "1.0"
|
|
2
|
+
# THE STORY, declared the way a component declares its props: each description is the brief
|
|
3
|
+
# the writer follows; the defaults are this org's own words, as they stand.
|
|
4
|
+
type: identity
|
|
5
|
+
name: story
|
|
6
|
+
category: Identity
|
|
7
|
+
props:
|
|
8
|
+
history:
|
|
9
|
+
type: string
|
|
10
|
+
input: true
|
|
11
|
+
description: >-
|
|
12
|
+
Where the enterprise came from, one paragraph of 60 to 120 words: founding year and
|
|
13
|
+
place, the names it has carried, the mergers and milestones its about or history page
|
|
14
|
+
states, in order. Only dated facts it gives; empty when it gives none.
|
|
15
|
+
maxLength: 900
|
|
16
|
+
default: ""
|
|
17
|
+
milestones:
|
|
18
|
+
type: array
|
|
19
|
+
input: true
|
|
20
|
+
description: >-
|
|
21
|
+
The story's beats, in order: one entry per dated event the enterprise itself records
|
|
22
|
+
(a founding, a merger, a name, a first, a listing), with the year as its name and one
|
|
23
|
+
sentence on what happened and why it mattered. From its history page, its timeline or
|
|
24
|
+
its annual report. Empty when it records none; never invented.
|
|
25
|
+
maxItems: 20
|
|
26
|
+
default: []
|
|
27
|
+
present:
|
|
28
|
+
type: string
|
|
29
|
+
input: true
|
|
30
|
+
description: >-
|
|
31
|
+
What the enterprise is now, one paragraph of 60 to 120 words: its scale as it states
|
|
32
|
+
it (customers, branches, countries, staff, assets), its main lines of business, its
|
|
33
|
+
position in its market in its own words, with the date those figures carry.
|
|
34
|
+
maxLength: 900
|
|
35
|
+
default: ""
|
|
36
|
+
vision:
|
|
37
|
+
type: string
|
|
38
|
+
input: true
|
|
39
|
+
description: >-
|
|
40
|
+
Where the enterprise is going, one paragraph: the vision, strategy or ambition it
|
|
41
|
+
publishes, quoted where a vision statement exists, with the horizon it names (a year,
|
|
42
|
+
a plan). Empty when it says nothing about the future.
|
|
43
|
+
maxLength: 900
|
|
44
|
+
default: ""
|
|
45
|
+
people:
|
|
46
|
+
type: string
|
|
47
|
+
input: true
|
|
48
|
+
description: >-
|
|
49
|
+
Whom the story is about and for: the people the enterprise names as the protagonists
|
|
50
|
+
of its story and the ones it tells it to (customers, employees, partners, a country, a
|
|
51
|
+
community), in its own words. Empty when it names none.
|
|
52
|
+
maxLength: 400
|
|
53
|
+
default: ""
|
|
54
|
+
meaning:
|
|
55
|
+
type: string
|
|
56
|
+
input: true
|
|
57
|
+
description: >-
|
|
58
|
+
What the story says the enterprise is for: one paragraph on how the history, the
|
|
59
|
+
present and the vision explain why it does what it does, connecting them to the
|
|
60
|
+
purpose (read purpose.yaml first). In the enterprise's own words where it makes the
|
|
61
|
+
connection; empty when it does not.
|
|
62
|
+
maxLength: 500
|
|
63
|
+
default: ""
|
package/local/scaffold.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* alike and disagree about what a starting component contains. So this file is plain ESM
|
|
12
12
|
* rather than TypeScript, because the CLI runs unbuilt and cannot transpile.
|
|
13
13
|
*
|
|
14
|
-
* See docs/architecture/LOCAL_STUDIO.md.
|
|
14
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md.
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync, mkdirSync, writeFileSync, cpSync, readdirSync, readFileSync } from "node:fs";
|
|
17
17
|
import { designHome } from "./design-home.mjs";
|
|
@@ -61,7 +61,7 @@ const WELCOME = [
|
|
|
61
61
|
].join("\n");
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* Create `design/<name>/` under `home`, with its
|
|
64
|
+
* Create `design/<name>/` under `home`, with its authoring folders, one component and the identity.
|
|
65
65
|
*
|
|
66
66
|
* Refuses an existing name rather than merging into it: "create" that silently adopts a
|
|
67
67
|
* folder is how a developer ends up publishing someone else's work under their own project.
|
|
@@ -77,7 +77,7 @@ export function createProject(home, name) {
|
|
|
77
77
|
// beside components/apps/styles so a skill is org-scoped exactly like a component —
|
|
78
78
|
// two orgs may each ship an `hr-education-coach` without colliding on one flat name
|
|
79
79
|
// (items/collect.ts qualifies a skill's row `<org>/<name>`).
|
|
80
|
-
for (const d of ["components/welcome", "apps", "styles", "skills", "blocks"]) mkdirSync(join(root, d), { recursive: true });
|
|
80
|
+
for (const d of ["components/welcome", "apps", "styles", "skills", "blocks", "identity"]) mkdirSync(join(root, d), { recursive: true });
|
|
81
81
|
writeFileSync(join(root, "components/welcome/welcome.yaml"), WELCOME);
|
|
82
82
|
|
|
83
83
|
// Worked example styles (sample-styles: brand ingredients + the override rules), so
|
|
@@ -90,6 +90,15 @@ export function createProject(home, name) {
|
|
|
90
90
|
cpSync(sampleStyles, join(root, "styles"), { recursive: true });
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// THE IDENTITY (sample-identity: organisation, brand, purpose, story), four definitions
|
|
94
|
+
// with their briefs written and their defaults empty, so the org's own words have a
|
|
95
|
+
// place to land from day one. Mirrors @unoverse-platform/base identitySeed.ts file for
|
|
96
|
+
// file (a guard test holds them equal). Same rule: copied only into an empty folder.
|
|
97
|
+
const sampleIdentity = join(dirname(fileURLToPath(import.meta.url)), "sample-identity");
|
|
98
|
+
if (existsSync(sampleIdentity) && readdirSync(join(root, "identity")).length === 0) {
|
|
99
|
+
cpSync(sampleIdentity, join(root, "identity"), { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
93
102
|
// A worked example APP (a generic chat: focus overlay + the chat region),
|
|
94
103
|
// so apps/ is never an empty folder and the first app is a rename away. Same
|
|
95
104
|
// rule as the styles: copied only into an empty folder, then the developer's.
|
|
@@ -185,11 +194,43 @@ export function topUpSamples(home, project) {
|
|
|
185
194
|
* Skills, prompt blocks and nodes are per-developer and not per-project, so they are made
|
|
186
195
|
* once here and never by `createProject`.
|
|
187
196
|
*/
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* EDITOR HELP FOR NODES, WITHOUT A LINE IN EVERY FILE (2026-09-05). The YAML language
|
|
200
|
+
* server maps files to schemas by glob from `.vscode/settings.json`, so a workspace that
|
|
201
|
+
* carries this mapping gets autocomplete and errors on every node file it will ever hold,
|
|
202
|
+
* and no file needs a `$schema:` key. The schemas are the docs site's, so the mapping
|
|
203
|
+
* stays true across releases. Written once, never over a developer's own settings.
|
|
204
|
+
*/
|
|
205
|
+
const SCHEMAS = "https://docs.unoverse.ai/schemas/nodes";
|
|
206
|
+
const NODE_SCHEMA_MAP = {
|
|
207
|
+
[`${SCHEMAS}/package.schema.json`]: ["nodes/*/package.yaml"],
|
|
208
|
+
[`${SCHEMAS}/credential.schema.json`]: ["nodes/*/credentials/*.yaml"],
|
|
209
|
+
[`${SCHEMAS}/node.schema.json`]: ["nodes/*/nodes/*/node.yaml"],
|
|
210
|
+
[`${SCHEMAS}/interface.schema.json`]: ["nodes/*/nodes/*/interface.yaml"],
|
|
211
|
+
[`${SCHEMAS}/config.schema.json`]: ["nodes/*/nodes/*/config.yaml"],
|
|
212
|
+
[`${SCHEMAS}/test.schema.json`]: ["nodes/*/nodes/*/test.yaml"],
|
|
213
|
+
[`${SCHEMAS}/run.schema.json`]: ["nodes/*/nodes/*/api/run.yaml"],
|
|
214
|
+
[`${SCHEMAS}/events.schema.json`]: ["nodes/*/nodes/*/api/events.yaml"],
|
|
215
|
+
[`${SCHEMAS}/service.schema.json`]: ["nodes/*/nodes/*/api/service.yaml"],
|
|
216
|
+
[`${SCHEMAS}/toolExchange.schema.json`]: ["nodes/*/nodes/*/api/toolExchange.yaml"],
|
|
217
|
+
[`${SCHEMAS}/audio.schema.json`]: ["nodes/*/nodes/*/api/audio.yaml"],
|
|
218
|
+
};
|
|
219
|
+
export function writeEditorSettings(home) {
|
|
220
|
+
const dir = join(home, ".vscode");
|
|
221
|
+
mkdirSync(dir, { recursive: true });
|
|
222
|
+
const settings = join(dir, "settings.json");
|
|
223
|
+
if (!existsSync(settings)) writeFileSync(settings, JSON.stringify({ "yaml.schemas": NODE_SCHEMA_MAP }, null, 2) + "\n");
|
|
224
|
+
const ext = join(dir, "extensions.json");
|
|
225
|
+
if (!existsSync(ext)) writeFileSync(ext, JSON.stringify({ recommendations: ["redhat.vscode-yaml"] }, null, 2) + "\n");
|
|
226
|
+
}
|
|
227
|
+
|
|
188
228
|
export function createFirstProject(home, name) {
|
|
189
229
|
const root = createProject(home, name);
|
|
190
230
|
// Only nodes/ remains beside the design tree; skills/ and blocks/ moved INTO the org
|
|
191
231
|
// (createProject makes them) so every authored kind sits in one place.
|
|
192
232
|
for (const d of ["nodes"]) mkdirSync(join(home, d), { recursive: true });
|
|
233
|
+
writeEditorSettings(home);
|
|
193
234
|
|
|
194
235
|
topUpSamples(home, name);
|
|
195
236
|
|
package/local/source.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* Everything else is readable and not writable, the marketplace and anything resolved out
|
|
21
21
|
* of node_modules included.
|
|
22
22
|
*
|
|
23
|
-
* See docs/architecture/LOCAL_STUDIO.md §Authoring in Studio.
|
|
23
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md §Authoring in Studio.
|
|
24
24
|
*/
|
|
25
25
|
import { designHome } from "./design-home.mjs";
|
|
26
26
|
import { resolve, sep, relative } from "path";
|
package/package.json
CHANGED
|
@@ -75,7 +75,7 @@ export function IdentityHeader({
|
|
|
75
75
|
{id && <div className="mt-0.5 font-mono text-[11px] text-gray-400">{id}</div>}
|
|
76
76
|
{description && <p className="mt-1 max-w-3xl text-sm leading-relaxed text-gray-600">{description}</p>}
|
|
77
77
|
{whenToUse && (
|
|
78
|
-
<div className="mt-2 max-w-3xl rounded-
|
|
78
|
+
<div className="mt-2 max-w-3xl rounded-lg px-3 py-2" style={{ background: "#f3f4f6" }}>
|
|
79
79
|
<div className="mb-0.5 text-[10px] font-semibold uppercase tracking-wider" style={{ color: accent }}>
|
|
80
80
|
When to use
|
|
81
81
|
</div>
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity view — the org's four documents (organisation, brand, purpose, story) as the
|
|
3
|
+
* pack holds them: design/<org>/identity/<doc>/. Left: a navigator, one group per org
|
|
4
|
+
* (or the one org this Studio is bound to). Right: the selected document's manifest words,
|
|
5
|
+
* then each prop with its brief and its default, the org's own words.
|
|
6
|
+
*
|
|
7
|
+
* READ-ONLY, like Skills: the documents are authored in the pack (the create skill fills
|
|
8
|
+
* them from the org's own sources) and published with it. The one control is the same
|
|
9
|
+
* master switch every other kind has: Available puts a document in the spatial catalogue,
|
|
10
|
+
* and each canvas installs it from there. The Organisation's switch is the grounding: on,
|
|
11
|
+
* every run as this org wakes with it; it never sits on the map
|
|
12
|
+
* (docs/edgy/elements/organisation.md).
|
|
13
|
+
*
|
|
14
|
+
* Shared by both Studios (packages/studio/screens); the host supplies fetch + toggles.
|
|
15
|
+
*/
|
|
16
|
+
import { useEffect, useMemo, useState } from "react";
|
|
17
|
+
import { fetchUnoverse } from "studio-host";
|
|
18
|
+
import { usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
|
|
19
|
+
import { IdentityHeader } from "./IdentityHeader";
|
|
20
|
+
import { DOC, EMPTY, NAV, NAV_SECTION, PANE, SCREEN, navItem } from "./chrome";
|
|
21
|
+
import { EmptyLibrary } from "./EmptyLibrary";
|
|
22
|
+
|
|
23
|
+
interface IdentityProp {
|
|
24
|
+
name: string;
|
|
25
|
+
type: string;
|
|
26
|
+
description: string;
|
|
27
|
+
maxLength?: number;
|
|
28
|
+
maxItems?: number;
|
|
29
|
+
default: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface IdentityItem {
|
|
33
|
+
/** The registry name, `<org>/<doc>`: the same word every other surface addresses it by. */
|
|
34
|
+
name: string;
|
|
35
|
+
org: string;
|
|
36
|
+
doc: "organisation" | "brand" | "purpose" | "story";
|
|
37
|
+
manifest: { title?: string; description?: string; whenToUse?: string; category?: string; version?: string };
|
|
38
|
+
props: IdentityProp[];
|
|
39
|
+
path: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ORDER = ["organisation", "brand", "purpose", "story"] as const;
|
|
43
|
+
const isEmpty = (v: unknown): boolean => v == null || (typeof v === "string" && !v.trim()) || (Array.isArray(v) && v.length === 0);
|
|
44
|
+
|
|
45
|
+
function Value({ value }: { value: unknown }) {
|
|
46
|
+
if (isEmpty(value)) return <div className="rounded-lg border border-dashed border-gray-200 px-4 py-3 text-[13px] italic text-gray-400">Not written yet.</div>;
|
|
47
|
+
if (typeof value === "string") return <p className="whitespace-pre-wrap text-[15px] leading-7 text-gray-900">{value}</p>;
|
|
48
|
+
if (Array.isArray(value)) return <Tree items={value} />;
|
|
49
|
+
return <pre className="rounded-lg bg-gray-50 p-3 text-xs text-gray-700">{JSON.stringify(value, null, 2)}</pre>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function Tree({ items, depth = 0 }: { items: unknown[]; depth?: number }) {
|
|
53
|
+
if (items.every((it) => typeof it === "string")) {
|
|
54
|
+
return (
|
|
55
|
+
<ul className="space-y-2">
|
|
56
|
+
{items.map((it, i) => (
|
|
57
|
+
<li key={i} className="flex gap-3 text-[15px] leading-7 text-gray-900">
|
|
58
|
+
<span className="mt-[11px] h-1.5 w-1.5 shrink-0 rounded-full bg-gray-300" />
|
|
59
|
+
<span>{String(it)}</span>
|
|
60
|
+
</li>
|
|
61
|
+
))}
|
|
62
|
+
</ul>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return (
|
|
66
|
+
<div className={depth ? "ml-5 mt-2" : "divide-y divide-gray-100 rounded-xl border border-gray-200"}>
|
|
67
|
+
{items.map((it, i) => {
|
|
68
|
+
const o = (it ?? {}) as Record<string, unknown>;
|
|
69
|
+
const kids = (o.units ?? o.goals ?? o.brands) as unknown[] | undefined;
|
|
70
|
+
const sentence = (o.what ?? o.why) as string | undefined;
|
|
71
|
+
return (
|
|
72
|
+
<div key={i} className={depth ? "py-2" : "px-4 py-3"}>
|
|
73
|
+
<div className="text-[14px] font-semibold text-gray-900">{String(o.name ?? "")}</div>
|
|
74
|
+
{sentence ? <div className="mt-0.5 text-[13.5px] leading-6 text-gray-600">{sentence}</div> : null}
|
|
75
|
+
{Array.isArray(kids) && kids.length > 0 ? <Tree items={kids} depth={depth + 1} /> : null}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
})}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One prop: its name, the value set as reading text, and the brief behind a small toggle. */
|
|
84
|
+
function Field({ prop }: { prop: IdentityProp }) {
|
|
85
|
+
const [brief, setBrief] = useState(false);
|
|
86
|
+
const limit = prop.maxItems ? `up to ${prop.maxItems}` : prop.maxLength ? `up to ${prop.maxLength} characters` : "";
|
|
87
|
+
return (
|
|
88
|
+
<section className="py-6 first:pt-0 last:pb-0">
|
|
89
|
+
<div className="mb-2 flex items-baseline justify-between gap-4">
|
|
90
|
+
<h3 className="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-500">{prop.name}</h3>
|
|
91
|
+
{prop.description ? (
|
|
92
|
+
<button type="button" onClick={() => setBrief((b) => !b)} className={`text-[11px] font-medium transition ${brief ? "text-indigo-600" : "text-gray-400 hover:text-gray-600"}`}>
|
|
93
|
+
{brief ? "Hide the brief" : "Brief"}
|
|
94
|
+
</button>
|
|
95
|
+
) : null}
|
|
96
|
+
</div>
|
|
97
|
+
{brief && prop.description ? (
|
|
98
|
+
<div className="mb-3 rounded-lg border border-indigo-100 bg-indigo-50/60 px-4 py-3 text-[12.5px] leading-relaxed text-indigo-900">
|
|
99
|
+
{prop.description}
|
|
100
|
+
{limit ? <div className="mt-1.5 text-[11px] text-indigo-500">{limit}</div> : null}
|
|
101
|
+
</div>
|
|
102
|
+
) : null}
|
|
103
|
+
<Value value={prop.default} />
|
|
104
|
+
</section>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function IdentityView({ org = "all" }: { org?: string } = {}) {
|
|
109
|
+
const [items, setItems] = useState<IdentityItem[]>([]);
|
|
110
|
+
const [error, setError] = useState<string | null>(null);
|
|
111
|
+
const [selected, setSelected] = usePersistentState<string | null>("wb:identity:selected", null);
|
|
112
|
+
const available = useRegistryToggles("identity");
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
fetchUnoverse(`/dev/identity${org !== "all" ? `?org=${encodeURIComponent(org)}` : ""}`)
|
|
116
|
+
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
|
|
117
|
+
.then((d) => {
|
|
118
|
+
const list: IdentityItem[] = d.items ?? [];
|
|
119
|
+
setItems(list);
|
|
120
|
+
const restore = list.find((i) => i.name === selected) ?? list[0];
|
|
121
|
+
setSelected(restore ? restore.name : null);
|
|
122
|
+
})
|
|
123
|
+
.catch((e) => setError(String(e?.message ?? e)));
|
|
124
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
125
|
+
}, [org]);
|
|
126
|
+
|
|
127
|
+
const byOrg = useMemo(
|
|
128
|
+
() =>
|
|
129
|
+
Object.entries(
|
|
130
|
+
items.reduce<Record<string, IdentityItem[]>>((acc, i) => {
|
|
131
|
+
(acc[i.org] ??= []).push(i);
|
|
132
|
+
return acc;
|
|
133
|
+
}, {}),
|
|
134
|
+
)
|
|
135
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
136
|
+
.map(([o, docs]) => [o, [...docs].sort((a, b) => ORDER.indexOf(a.doc) - ORDER.indexOf(b.doc))] as const),
|
|
137
|
+
[items],
|
|
138
|
+
);
|
|
139
|
+
const current = items.find((i) => i.name === selected) ?? null;
|
|
140
|
+
|
|
141
|
+
if (items.length === 0) {
|
|
142
|
+
return (
|
|
143
|
+
<div className={SCREEN}>
|
|
144
|
+
<nav className={NAV} />
|
|
145
|
+
<EmptyLibrary
|
|
146
|
+
noun="identity documents"
|
|
147
|
+
org={org}
|
|
148
|
+
folder="identity"
|
|
149
|
+
shape="identity/ holds four definitions: organisation, brand, purpose and story, each a manifest and a document whose props carry the brief and the org's own words."
|
|
150
|
+
error={error}
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className={SCREEN}>
|
|
158
|
+
<nav className={NAV}>
|
|
159
|
+
{byOrg.map(([o, docs]) => (
|
|
160
|
+
<div key={o} className="mb-3">
|
|
161
|
+
<div className={NAV_SECTION}>{o}</div>
|
|
162
|
+
{docs.map((d) => (
|
|
163
|
+
<button key={d.name} onClick={() => setSelected(d.name)} className={`block w-full rounded px-2 py-1.5 text-left transition ${navItem(selected === d.name)}`}>
|
|
164
|
+
<div className="text-sm font-medium">{d.manifest.title ?? d.doc}</div>
|
|
165
|
+
<div className="truncate text-[11px] text-gray-500">{d.manifest.description}</div>
|
|
166
|
+
</button>
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
))}
|
|
170
|
+
</nav>
|
|
171
|
+
|
|
172
|
+
<div className={PANE}>
|
|
173
|
+
{!current ? (
|
|
174
|
+
<div className={EMPTY}>Select a document</div>
|
|
175
|
+
) : (
|
|
176
|
+
<div className={DOC}>
|
|
177
|
+
<IdentityHeader
|
|
178
|
+
name={current.name}
|
|
179
|
+
category={current.manifest.category}
|
|
180
|
+
version={current.manifest.version}
|
|
181
|
+
id={`design/${current.org}/${current.path}`}
|
|
182
|
+
description={current.manifest.description}
|
|
183
|
+
whenToUse={current.manifest.whenToUse}
|
|
184
|
+
right={<ToggleSwitch on={available.isEnabled(current.name)} onClick={() => available.toggle(current.name)} label={current.doc === "organisation" ? "Grounding" : "Available"} />}
|
|
185
|
+
/>
|
|
186
|
+
<p className="mb-6 text-[12px] leading-relaxed text-gray-500">
|
|
187
|
+
{current.doc === "organisation"
|
|
188
|
+
? "Grounding on means every extraction and every Agent that runs as this org reads this document whole: its what and its vocabulary. Off, the org grounds on nothing."
|
|
189
|
+
: "Available puts this document in the spatial catalogue. Which maps carry it is chosen per canvas, in Canvas’s Content Library."}
|
|
190
|
+
{" "}Written in the pack; the create skill fills it from the org’s own sources.
|
|
191
|
+
</p>
|
|
192
|
+
<p className="mb-6 text-[11px] text-gray-500">
|
|
193
|
+
Reference in a prompt as <span className="font-mono text-gray-600">{`{{identity.${current.doc}}}`}</span> for the whole document
|
|
194
|
+
{current.props[0] ? <>, or one field as <span className="font-mono text-gray-600">{`{{identity.${current.doc}.${current.props[0].name}}}`}</span></> : null}.
|
|
195
|
+
Every document at once is <span className="font-mono text-gray-600">{"{{identity}}"}</span>.
|
|
196
|
+
</p>
|
|
197
|
+
|
|
198
|
+
<div className="divide-y divide-gray-100">
|
|
199
|
+
{current.props.map((p) => <Field key={p.name} prop={p} />)}
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
package/screens/SourceView.tsx
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* lost on the next install, and the design system is what keeps the token contract stable.
|
|
20
20
|
* The verdict is the host's, never a rule reimplemented here.
|
|
21
21
|
*
|
|
22
|
-
* See docs/architecture/LOCAL_STUDIO.md §Authoring in Studio.
|
|
22
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md §Authoring in Studio.
|
|
23
23
|
*/
|
|
24
24
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
25
25
|
import { Compartment, EditorState } from "@codemirror/state";
|
package/screens/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* studio: the Studio screens, owned once and imported by BOTH Studios,
|
|
3
3
|
* the Canvas-embedded catalog and the standalone local authoring app.
|
|
4
|
-
* See docs/architecture/LOCAL_STUDIO.md.
|
|
4
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md.
|
|
5
5
|
*
|
|
6
6
|
* These screens contain NO transport, auth or persistence of their own. Everything
|
|
7
7
|
* host-specific is imported from the bare specifier `studio-host`, which each app
|
|
@@ -24,6 +24,7 @@ export { TemplatesView } from "./TemplatesView";
|
|
|
24
24
|
export { TemplatesLibraryView } from "./TemplatesLibraryView";
|
|
25
25
|
export { DesignSystemView } from "./DesignSystemView";
|
|
26
26
|
export { SkillsView } from "./SkillsView";
|
|
27
|
+
export { IdentityView } from "./IdentityView";
|
|
27
28
|
export { PromptBlocksView } from "./PromptBlocksView";
|
|
28
29
|
export { IdentityHeader } from "./IdentityHeader";
|
|
29
30
|
export { stateActivation } from "./states";
|
|
@@ -32,5 +33,5 @@ export { stateActivation } from "./states";
|
|
|
32
33
|
// so it is shared even though neither shares the screen around it.
|
|
33
34
|
export { ConfigForm, defaultsFromSchema } from "./ConfigForm";
|
|
34
35
|
// The source view renders only where the host grants source access, so it is exported
|
|
35
|
-
// unconditionally and returns null in Canvas (docs/architecture/LOCAL_STUDIO.md).
|
|
36
|
+
// unconditionally and returns null in Canvas (docs/architecture/authoring/LOCAL_STUDIO.md).
|
|
36
37
|
export { SourceView } from "./SourceView";
|
package/src/App.tsx
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Studio renders. This file only does what a host must: pick the project, pick the
|
|
6
6
|
* theme variant, resolve the theme, and choose which screen is showing.
|
|
7
7
|
*
|
|
8
|
-
* See docs/architecture/LOCAL_STUDIO.md.
|
|
8
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md.
|
|
9
9
|
*/
|
|
10
10
|
import { useEffect, useState } from "react";
|
|
11
11
|
import type { ResolvedTheme } from "@unoverse/sdk";
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
TemplatesLibraryView,
|
|
17
17
|
DesignSystemView,
|
|
18
18
|
SkillsView,
|
|
19
|
+
IdentityView,
|
|
19
20
|
PromptBlocksView,
|
|
20
21
|
} from "@unoverse/studio";
|
|
21
22
|
import { client, usePersistentState } from "./host";
|
|
@@ -33,10 +34,11 @@ import {
|
|
|
33
34
|
Sparkles,
|
|
34
35
|
SquareDashed,
|
|
35
36
|
TextQuote,
|
|
37
|
+
Landmark,
|
|
36
38
|
type LucideIcon,
|
|
37
39
|
} from "lucide-react";
|
|
38
40
|
|
|
39
|
-
type View = "apps" | "templates" | "components" | "atoms" | "tokens" | "skills" | "prompts" | "nodes";
|
|
41
|
+
type View = "apps" | "templates" | "components" | "atoms" | "tokens" | "identity" | "skills" | "prompts" | "nodes";
|
|
40
42
|
|
|
41
43
|
const NAV: { key: View; label: string; group: string; Icon: LucideIcon }[] = [
|
|
42
44
|
{ key: "apps", label: "Apps", group: "Build", Icon: LayoutDashboard },
|
|
@@ -44,6 +46,7 @@ const NAV: { key: View; label: string; group: string; Icon: LucideIcon }[] = [
|
|
|
44
46
|
{ key: "components", label: "Components", group: "Build", Icon: SquareDashed },
|
|
45
47
|
{ key: "atoms", label: "Atoms", group: "Build", Icon: Atom },
|
|
46
48
|
{ key: "tokens", label: "Styles", group: "Build", Icon: Palette },
|
|
49
|
+
{ key: "identity", label: "Identity", group: "AI", Icon: Landmark },
|
|
47
50
|
{ key: "skills", label: "Skills", group: "AI", Icon: Sparkles },
|
|
48
51
|
{ key: "prompts", label: "Prompt Blocks", group: "AI", Icon: TextQuote },
|
|
49
52
|
{ key: "nodes", label: "Nodes", group: "Code", Icon: Boxes },
|
|
@@ -201,6 +204,8 @@ function Shell() {
|
|
|
201
204
|
<AtomsView theme={theme} />
|
|
202
205
|
) : view === "tokens" ? (
|
|
203
206
|
<DesignSystemView theme={theme} foundation={foundation} org={org} />
|
|
207
|
+
) : view === "identity" ? (
|
|
208
|
+
<IdentityView org={org} />
|
|
204
209
|
) : view === "skills" ? (
|
|
205
210
|
<SkillsView org={org} />
|
|
206
211
|
) : view === "nodes" ? (
|
package/src/NodeKeys.tsx
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* So this panel is read-only. It names the exact variable each field wants and says
|
|
10
10
|
* whether it is set. Entering the value happens in the editor they are already in.
|
|
11
11
|
*
|
|
12
|
-
* See docs/architecture/LOCAL_STUDIO.md.
|
|
12
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md.
|
|
13
13
|
*/
|
|
14
14
|
import { useEffect, useState } from "react";
|
|
15
15
|
|
package/src/host.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* There is no auth here on purpose. Local Studio is offline and single-user: the files
|
|
10
10
|
* are already yours. Authentication belongs to a universe, not to a folder.
|
|
11
11
|
*
|
|
12
|
-
* See docs/architecture/LOCAL_STUDIO.md.
|
|
12
|
+
* See docs/architecture/authoring/LOCAL_STUDIO.md.
|
|
13
13
|
*/
|
|
14
14
|
import { useCallback, useEffect, useState } from "react";
|
|
15
15
|
import type { ResolvedTheme, UnoverseDefinition, UnoverseClient } from "@unoverse/sdk";
|
|
@@ -44,7 +44,7 @@ export function liveUniverseUrl(): string | null {
|
|
|
44
44
|
* The capability lives on the host for the reason `hasAuth` does: a shared screen must
|
|
45
45
|
* never branch on which app it is running in. Canvas leaves it false, so hosted surfaces
|
|
46
46
|
* render and never edit, by construction rather than by discipline
|
|
47
|
-
* (docs/architecture/LOCAL_STUDIO.md §The capability gate).
|
|
47
|
+
* (docs/architecture/authoring/LOCAL_STUDIO.md §The capability gate).
|
|
48
48
|
*/
|
|
49
49
|
export const hasSourceAccess = true;
|
|
50
50
|
|
|
@@ -187,7 +187,7 @@ export function usePersistentState<T>(key: string, initial: T): [T, React.Dispat
|
|
|
187
187
|
return [value, setValue];
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
export type RegistryKind = "node" | "skill" | "app" | "prompt" | "component" | "template";
|
|
190
|
+
export type RegistryKind = "node" | "skill" | "app" | "prompt" | "component" | "template" | "identity";
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
193
|
* Registry toggles decide what a RUNNING universe exposes; they persist to
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* (config.enableUserMemory) is ON, each completed turn (original user input +
|
|
6
6
|
* final agent answer — never intermediate iterations or tool chatter) is
|
|
7
7
|
* POSTed to the memory server's /memory/ingest, which feeds the L1→L4
|
|
8
|
-
* cascade (see docs/architecture/MEMORY_SPEC.md). Replaces the retired
|
|
8
|
+
* cascade (see docs/architecture/memory/MEMORY_SPEC.md). Replaces the retired
|
|
9
9
|
* GravityMemoryStore canvas node.
|
|
10
10
|
*
|
|
11
11
|
* Every agent family calls this at turn completion with its own node type;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE IDENTITY SEED: design/<org>/identity/, four definitions with their briefs written and
|
|
3
|
+
* their defaults empty. GENERATED from the one source (the identity-defs script in the
|
|
4
|
+
* session that ruled it, 2026-09-05) and mirrored file for file by
|
|
5
|
+
* packages/studio/local/sample-identity/, which the Studio scaffold copies; a guard test
|
|
6
|
+
* holds the two equal. Edit the briefs here and there together, or regenerate both.
|
|
7
|
+
*/
|
|
8
|
+
export const IDENTITY_SEED: Readonly<Record<string, string>> = {
|
|
9
|
+
"organisation/manifest.yaml": "# The Organisation (docs/edgy/elements/organisation.md): the one that owns this spatial. This\n# manifest says what the document is for; organisation.yaml is the document. It never sits\n# on the map: it is the scope on every row and the grounding every extraction and every\n# agent wakes with.\ntitle: Organisation\ndescription: >-\n Who this organisation is and what it does, in its own words, and the words it uses for its\n products, its customers and itself.\nwhenToUse: >-\n Ground any extraction, any agent or any piece of writing in who the organisation is and\n how it speaks. Read whole at wake, never searched for. Not for what it offers, that is the\n products on the map, and not for why it exists or where it came from, those are Purpose\n and Story.\ncategory: Identity\nversion: 1.0.0\n",
|
|
10
|
+
"organisation/organisation.yaml": "unoverse: \"1.0\"\n# THE ORGANISATION, declared the way a component declares its props. Each prop's description\n# is the brief: direction to the writer that fills it, including what not to do. The\n# platform compiles briefs into a tool schema, so an Agent fills this from research the way\n# a copywriter fills an email, and a person reads the same words as guidance. The defaults\n# are this org's own words, as they stand.\ntype: identity\nname: organisation\ncategory: Identity\nprops:\n name:\n type: string\n input: true\n description: >-\n The org's name as it trades, exactly as its website writes it in its own header or\n footer. Never a legal suffix unless the site uses one.\n maxLength: 80\n default: \"\"\n what:\n type: string\n input: true\n description: >-\n One paragraph, 60 to 120 words, in the third person: what the business is, what it\n does, whom it serves, where. Taken from the site's own about, home and footer copy.\n This paragraph grounds every extraction, so it must name the business's actual lines\n of work, not adjectives. Never a slogan, never an industry generality.\n maxLength: 900\n default: \"\"\n units:\n type: array\n input: true\n description: >-\n The organisation map: every unit the organisation names in its own publications, the\n ones that face customers (divisions, lines of business, regions) and the ones that run\n the organisation (operations, technology, people and HR, finance, risk, legal,\n marketing, and the rest). Sources: the about and careers pages, the annual report's\n organisation or leadership section, a published org chart. One entry per unit with its\n name as the organisation writes it and one sentence on what it does or serves, nested\n in `units` where the organisation nests them. Empty when the sources name none; never\n invented.\n maxItems: 40\n default: []\n vocabulary:\n type: string\n input: true\n description: >-\n The words the business uses, as one paragraph separated by semicolons: its product and\n service names as it spells them, its programme and membership names, the terms it uses\n for its customers, and anything it capitalises. Every name is copied from the site,\n never paraphrased. This is the domain prompt every extraction reads, so completeness\n beats elegance.\n maxLength: 2000\n default: \"\"\n",
|
|
11
|
+
"brand/manifest.yaml": "# The Brand (docs/edgy/elements/brand.md): our name and what it stands for. The written half;\n# styles/ is the visual half. Published, it is one row on the org's map, found by meaning.\ntitle: Brand\ndescription: >-\n The name and what it stands for: the promise it makes, whom it speaks to, what sets it\n apart, the brands inside it, and how it sounds.\nwhenToUse: >-\n Answer what the brand stands for, what to expect from it, whom it is for and what sets it\n apart; name the brands inside it; and give any writing its voice, the rules it follows and\n sentences in its own words. Not for what the organisation is or does, that is the\n Organisation, and not for why it exists, that is the Purpose.\ncategory: Identity\nversion: 1.0.0\n",
|
|
12
|
+
"brand/brand.yaml": "unoverse: \"1.0\"\n# THE BRAND, declared the way a component declares its props: each description is the brief\n# the writer follows; the defaults are this org's own words, as they stand.\ntype: identity\nname: brand\ncategory: Identity\nprops:\n name:\n type: string\n input: true\n description: >-\n The brand name as the enterprise presents it, which may differ from its trading name\n (a bank's card brand, a group's consumer brand). The site's logo text or title, not\n the legal entity.\n maxLength: 80\n default: \"\"\n standsFor:\n type: string\n input: true\n description: >-\n What the name stands for, in one sentence, in the enterprise's own words where it says\n so (a tagline, a strapline, the meaning of the name, a 'who we are' line). Empty when\n it does not say.\n maxLength: 300\n default: \"\"\n promise:\n type: string\n input: true\n description: >-\n The expectation the brand sets: what people can expect from it, in one or two\n sentences, drawn from the promises the site repeats on its home, about and brand\n pages. Written to the customer.\n maxLength: 400\n default: \"\"\n represents:\n type: string\n input: true\n description: >-\n How the brand carries the purpose (read purpose.yaml first): in one paragraph, how the\n name, the promise and the way it presents itself express why the enterprise exists. In\n the enterprise's own words where it connects the two; empty when it does not.\n maxLength: 500\n default: \"\"\n audience:\n type: string\n input: true\n description: >-\n Whom the brand speaks to and how it wants to be seen by them, in one or two sentences,\n from the way the site addresses its customers and describes its position. Never\n invented segments.\n maxLength: 400\n default: \"\"\n differentiators:\n type: array\n input: true\n description: >-\n What the enterprise claims sets its brand apart, three to six items, each in its own\n words (a first, a leadership claim, a partnership, a heritage). Only claims it makes\n itself.\n maxItems: 6\n default: []\n brands:\n type: array\n input: true\n description: >-\n The brand tree: the sub-brands and product brands the enterprise presents under this\n brand (a card family, an app, a loyalty programme, a subsidiary), one entry per brand\n with its name as written and one sentence on what it is for, nested in `brands` where\n the enterprise nests them. Empty when it presents none; never invented.\n maxItems: 20\n default: []\n rules:\n type: array\n input: true\n description: >-\n Three to six rules the site's copy visibly follows, each as 'Do this' or 'Never that':\n the person it addresses (you, we), its register (plain, formal, warm), sentence\n length, what it capitalises, what it never says.\n maxItems: 6\n default: []\n examples:\n type: array\n input: true\n description: >-\n Three to five sentences copied verbatim from the site that show the voice. Quotes,\n never paraphrase.\n maxItems: 5\n default: []\n",
|
|
13
|
+
"purpose/manifest.yaml": "# The Purpose (docs/edgy/elements/purpose.md): why the enterprise exists and the goals it pursues.\n# Published, it is one row on the org's map, found by meaning.\ntitle: Purpose\ndescription: >-\n Why the organisation exists: what its people believe in, what it aims to achieve, and the\n goals it is working toward.\nwhenToUse: >-\n Answer why the organisation exists, what its people believe in, what it aims to achieve\n and the goals it is working toward. Read before the Brand, which represents it. Not for\n its history or its scale, that is the Story, and not for its products.\ncategory: Identity\nversion: 1.0.0\n",
|
|
14
|
+
"purpose/purpose.yaml": "unoverse: \"1.0\"\n# THE PURPOSE, declared the way a component declares its props: each description is the brief\n# the writer follows; the defaults are this org's own words, as they stand.\ntype: identity\nname: purpose\ncategory: Identity\nprops:\n statement:\n type: string\n input: true\n description: >-\n Why the enterprise exists, in its own words: the purpose or mission statement it\n publishes, quoted where one exists. When it publishes none, one sentence composed only\n from what its about page says it is for. Never a marketing slogan.\n maxLength: 400\n default: \"\"\n values:\n type: array\n input: true\n description: >-\n What the people of the enterprise believe in and consider valuable: the values it\n publishes, each with its name as the enterprise writes it and the one sentence it\n gives for it. From the values, culture or about pages. Empty when none are published;\n never invented.\n maxItems: 8\n default: []\n ambition:\n type: string\n input: true\n description: >-\n The far end of the purpose: what the enterprise says it aims to achieve for its\n customers, its country or society, in one paragraph, quoted where a vision or ambition\n statement exists, with the horizon it names. Not the goals below; the outcome they\n serve.\n maxLength: 700\n default: \"\"\n goals:\n type: array\n input: true\n description: >-\n The goal tree: the strategic goals, pillars or commitments the enterprise names (a\n strategy page, a strategy section of the annual report, 'our commitments'). One entry\n per goal with its name as the enterprise writes it and one sentence on why, nested in\n `goals` where it nests them. Empty when none are named; never invented.\n maxItems: 12\n default: []\n",
|
|
15
|
+
"story/manifest.yaml": "# The Story (docs/edgy/elements/story.md): the way the enterprise makes sense of itself.\n# History, present, vision. Published, it is one row on the org's map, found by meaning.\ntitle: Story\ndescription: >-\n Where the organisation came from, what it is now and where it is going, and what that\n story says it is for.\nwhenToUse: >-\n Answer when and how the organisation began, its milestones, what it is today (its scale,\n its lines of business), where it says it is going, and whom its story is about. Read with\n the Purpose, which it puts in context. Not for why it exists, that is the Purpose.\ncategory: Identity\nversion: 1.0.0\n",
|
|
16
|
+
"story/story.yaml": "unoverse: \"1.0\"\n# THE STORY, declared the way a component declares its props: each description is the brief\n# the writer follows; the defaults are this org's own words, as they stand.\ntype: identity\nname: story\ncategory: Identity\nprops:\n history:\n type: string\n input: true\n description: >-\n Where the enterprise came from, one paragraph of 60 to 120 words: founding year and\n place, the names it has carried, the mergers and milestones its about or history page\n states, in order. Only dated facts it gives; empty when it gives none.\n maxLength: 900\n default: \"\"\n milestones:\n type: array\n input: true\n description: >-\n The story's beats, in order: one entry per dated event the enterprise itself records\n (a founding, a merger, a name, a first, a listing), with the year as its name and one\n sentence on what happened and why it mattered. From its history page, its timeline or\n its annual report. Empty when it records none; never invented.\n maxItems: 20\n default: []\n present:\n type: string\n input: true\n description: >-\n What the enterprise is now, one paragraph of 60 to 120 words: its scale as it states\n it (customers, branches, countries, staff, assets), its main lines of business, its\n position in its market in its own words, with the date those figures carry.\n maxLength: 900\n default: \"\"\n vision:\n type: string\n input: true\n description: >-\n Where the enterprise is going, one paragraph: the vision, strategy or ambition it\n publishes, quoted where a vision statement exists, with the horizon it names (a year,\n a plan). Empty when it says nothing about the future.\n maxLength: 900\n default: \"\"\n people:\n type: string\n input: true\n description: >-\n Whom the story is about and for: the people the enterprise names as the protagonists\n of its story and the ones it tells it to (customers, employees, partners, a country, a\n community), in its own words. Empty when it names none.\n maxLength: 400\n default: \"\"\n meaning:\n type: string\n input: true\n description: >-\n What the story says the enterprise is for: one paragraph on how the history, the\n present and the vision explain why it does what it does, connecting them to the\n purpose (read purpose.yaml first). In the enterprise's own words where it makes the\n connection; empty when it does not.\n maxLength: 500\n default: \"\"\n"
|
|
17
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ORG'S IDENTITY DOCUMENTS, READ (docs/unoverse/UNOVERSE_ORGANISATION.md section 5).
|
|
3
|
+
*
|
|
4
|
+
* Four definitions in design/<slug>/identity/, each a folder like a component's:
|
|
5
|
+
*
|
|
6
|
+
* identity/<doc>/manifest.yaml title, description, whenToUse, category, version
|
|
7
|
+
* identity/<doc>/<doc>.yaml type: identity, props: each with its brief and its default
|
|
8
|
+
*
|
|
9
|
+
* The defaults ARE the document: this org's own words. The briefs are the instructions a
|
|
10
|
+
* writer (an Agent with the create skill, or a person) follows to fill them. This module
|
|
11
|
+
* only reads; Studio authors, the platform renders. Nothing here writes a byte.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { parse } from "yaml";
|
|
16
|
+
|
|
17
|
+
export const ORG_DOCUMENTS = ["organisation", "brand", "purpose", "story"] as const;
|
|
18
|
+
export type OrgDocument = (typeof ORG_DOCUMENTS)[number];
|
|
19
|
+
|
|
20
|
+
export interface IdentityProp {
|
|
21
|
+
name: string;
|
|
22
|
+
type: string;
|
|
23
|
+
/** The brief: what belongs in the field, as direction to a writer. */
|
|
24
|
+
description: string;
|
|
25
|
+
maxLength?: number;
|
|
26
|
+
maxItems?: number;
|
|
27
|
+
/** The org's own words, as they stand. */
|
|
28
|
+
default: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface IdentityDocument {
|
|
32
|
+
manifest: { title?: string; description?: string; whenToUse?: string; category?: string; version?: string };
|
|
33
|
+
definition: { type?: string; name?: string; category?: string };
|
|
34
|
+
props: IdentityProp[];
|
|
35
|
+
/** prop name -> default, the document as a plain object. */
|
|
36
|
+
values: Record<string, unknown>;
|
|
37
|
+
/** Where it lives in the pack, relative to design/<slug>/. */
|
|
38
|
+
path: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** design/<slug>/identity/<doc>/ */
|
|
42
|
+
export const identityDir = (root: string, doc: OrgDocument): string => join(root, "identity", doc);
|
|
43
|
+
|
|
44
|
+
/** One identity definition as the pack holds it. Null when the folder is missing. */
|
|
45
|
+
export function readOrgDocument(root: string, doc: OrgDocument): IdentityDocument | null {
|
|
46
|
+
const dir = identityDir(root, doc);
|
|
47
|
+
const file = join(dir, `${doc}.yaml`);
|
|
48
|
+
if (!existsSync(file)) return null;
|
|
49
|
+
const definition = (parse(readFileSync(file, "utf8")) as Record<string, any>) ?? {};
|
|
50
|
+
const manifestFile = join(dir, "manifest.yaml");
|
|
51
|
+
const manifest = existsSync(manifestFile) ? ((parse(readFileSync(manifestFile, "utf8")) as Record<string, any>) ?? {}) : {};
|
|
52
|
+
const props: IdentityProp[] = Object.entries((definition.props ?? {}) as Record<string, any>).map(([name, p]) => ({
|
|
53
|
+
name,
|
|
54
|
+
type: String(p?.type ?? "string"),
|
|
55
|
+
description: String(p?.description ?? ""),
|
|
56
|
+
...(p?.maxLength != null ? { maxLength: Number(p.maxLength) } : {}),
|
|
57
|
+
...(p?.maxItems != null ? { maxItems: Number(p.maxItems) } : {}),
|
|
58
|
+
default: p?.default,
|
|
59
|
+
}));
|
|
60
|
+
return {
|
|
61
|
+
manifest: { title: manifest.title, description: manifest.description, whenToUse: manifest.whenToUse, category: manifest.category, version: manifest.version },
|
|
62
|
+
definition: { type: definition.type, name: definition.name, category: definition.category },
|
|
63
|
+
props,
|
|
64
|
+
values: Object.fromEntries(props.map((p) => [p.name, p.default])),
|
|
65
|
+
path: `identity/${doc}/${doc}.yaml`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CREATE AN ORG'S DESIGN PACK (docs/unoverse/UNOVERSE_ORGANISATION.md section 5).
|
|
3
|
+
*
|
|
4
|
+
* The one implementation, used by the `new-org` script and by the server's POST /dev/orgs.
|
|
5
|
+
* The slug is the one word for the org everywhere: this folder, the URL, the org Studio
|
|
6
|
+
* publishes into, the world a run gets. The pack holds:
|
|
7
|
+
*
|
|
8
|
+
* design/<slug>/
|
|
9
|
+
* identity/ the Identity facet (owner ruling 2026-09-05): a Studio asset
|
|
10
|
+
* organisation.yaml the org itself: name, what, units, vocabulary
|
|
11
|
+
* brand.yaml the written half of the brand (styles/ is the visual half)
|
|
12
|
+
* purpose.yaml why it exists
|
|
13
|
+
* story.yaml history, present, vision
|
|
14
|
+
* apps/ components/ the org's own
|
|
15
|
+
* styles/themes/ brand themes seeded from the marketplace foundation; base and
|
|
16
|
+
* semantic tokens are INHERITED at resolve time (an org is a theme)
|
|
17
|
+
*
|
|
18
|
+
* Shapes: each definition declares its own props with their briefs (identitySeed.ts). Nothing is
|
|
19
|
+
* invented: the defaults are empty until a writer fills them from the org's own sources.
|
|
20
|
+
*/
|
|
21
|
+
import { cpSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { dirname, join } from "node:path";
|
|
23
|
+
import { IDENTITY_SEED } from "./identitySeed.js";
|
|
24
|
+
|
|
25
|
+
export const ORG_SLUG = /^[a-z][a-z0-9-]{0,63}$/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Make design/<slug>/ under `designHome`. Throws if the slug is malformed or the pack
|
|
29
|
+
* exists; never overwrites. Returns the pack's path.
|
|
30
|
+
*/
|
|
31
|
+
export function createOrgPack(designHome: string, slug: string, name: string): string {
|
|
32
|
+
if (!ORG_SLUG.test(slug)) throw new Error("slug: lowercase letters, digits and hyphens, starting with a letter");
|
|
33
|
+
const dest = join(designHome, slug);
|
|
34
|
+
if (existsSync(dest)) throw new Error(`design/${slug} already exists`);
|
|
35
|
+
const themes = join(designHome, "marketplace", "styles", "themes");
|
|
36
|
+
if (!existsSync(themes)) throw new Error("the marketplace theme foundation is missing; cannot seed the org's themes");
|
|
37
|
+
|
|
38
|
+
mkdirSync(join(dest, "apps"), { recursive: true });
|
|
39
|
+
mkdirSync(join(dest, "components"), { recursive: true });
|
|
40
|
+
mkdirSync(join(dest, "identity"), { recursive: true });
|
|
41
|
+
cpSync(themes, join(dest, "styles", "themes"), { recursive: true });
|
|
42
|
+
// THE IDENTITY SEED: four definitions, briefs written, defaults empty (identitySeed.ts,
|
|
43
|
+
// mirrored by the Studio scaffold's sample-identity). The name is the org row's, not the
|
|
44
|
+
// document's: the writer fills `name` from the organisation's own site.
|
|
45
|
+
for (const [rel, text] of Object.entries(IDENTITY_SEED)) {
|
|
46
|
+
mkdirSync(dirname(join(dest, "identity", rel)), { recursive: true });
|
|
47
|
+
writeFileSync(join(dest, "identity", rel), text);
|
|
48
|
+
}
|
|
49
|
+
return dest;
|
|
50
|
+
}
|
|
@@ -108,7 +108,8 @@ export function collectProject(designRoot: string, project: string): CollectedIt
|
|
|
108
108
|
// A prompt block is NEVER qualified this way — see the blocks/ walk below for why.
|
|
109
109
|
// Apps keep their bare ids: those are org-qualified by convention
|
|
110
110
|
// (`<org>-chat`). docs/unoverse/UNOVERSE_COMPONENT_ORGS.md.
|
|
111
|
-
|
|
111
|
+
// `identity` (2026-09-05): the org's four documents, `<org>/brand` like a component.
|
|
112
|
+
const QUALIFIED_KINDS = new Set(["component", "skill", "template", "identity"]);
|
|
112
113
|
const add = (kind: string, name: string, definition: CollectedItem["definition"]) =>
|
|
113
114
|
items.push({
|
|
114
115
|
kind,
|
|
@@ -134,7 +135,10 @@ export function collectProject(designRoot: string, project: string): CollectedIt
|
|
|
134
135
|
// 029): apps publish as kind `app` and the Template Model kind owns the bare
|
|
135
136
|
// word. Forgetting a design dir here ships deploys that silently drop every
|
|
136
137
|
// artifact of that kind.
|
|
137
|
-
|
|
138
|
+
// IDENTITY (owner ruling 2026-09-05): design/<org>/identity/<doc>/ publishes as kind
|
|
139
|
+
// `identity`, the same walk as a component folder, so it reaches the registry, the
|
|
140
|
+
// spatial catalogue and the map through the one publish path everything else takes.
|
|
141
|
+
for (const [dir, kind] of [["components", "component"], ["apps", "app"], ["templates", "template"], ["atoms", "atom"], ["identity", "identity"]] as const) {
|
|
138
142
|
const home = join(root, dir);
|
|
139
143
|
if (!existsSync(home)) continue;
|
|
140
144
|
for (const e of readdirSync(home, { withFileTypes: true })) {
|
|
@@ -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", "
|
|
60
|
+
"tagline", "shortDescription", "introParagraph", "callToAction", "primaryImage", "images", "action",
|
|
61
61
|
"bodyCopy", "features", "mainCategory", "section", "needs",
|
|
62
62
|
]);
|
|
63
63
|
|
|
@@ -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. */
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* scripts/lib/lint-nodes.mjs, which must run without the server (it ships in the
|
|
14
14
|
* starter kit). Keep the two in step; the linter is the one developers see first.
|
|
15
15
|
*
|
|
16
|
-
* See docs/architecture/DECLARATIVE_NODES.md §5.
|
|
16
|
+
* See docs/architecture/authoring/DECLARATIVE_NODES.md §5.
|
|
17
17
|
*/
|
|
18
18
|
import * as YAML from "yaml";
|
|
19
19
|
import type { RawNode, RawPackage } from "./source.js";
|
|
@@ -59,6 +59,9 @@ export function contextFor(node: ComposedNode, inputs: any, config: any, executi
|
|
|
59
59
|
scope: {
|
|
60
60
|
userId: executionContext?.publishingContext?.userId ?? executionContext?.workflow?.variables?.userId,
|
|
61
61
|
workflowId: executionContext?.workflowId ?? executionContext?.workflow?.id,
|
|
62
|
+
// THE WORLD THIS RUN WORKS IN (docs/unoverse/UNOVERSE_SPATIAL_ORG.md): named by the run,
|
|
63
|
+
// else the canvas's default. The spatial and ingest packages send it as the map's scope.
|
|
64
|
+
org: executionContext?.publishingContext?.org ?? executionContext?.workflow?.org,
|
|
62
65
|
conversationId:
|
|
63
66
|
executionContext?.publishingContext?.conversationId ?? executionContext?.workflow?.variables?.conversationId,
|
|
64
67
|
chatId: executionContext?.publishingContext?.chatId ?? executionContext?.workflow?.variables?.chatId,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* updating a node should never restart a universe. When the source becomes Postgres,
|
|
12
12
|
* an install is a row write followed by this call.
|
|
13
13
|
*
|
|
14
|
-
* See docs/architecture/DECLARATIVE_NODES.md.
|
|
14
|
+
* See docs/architecture/authoring/DECLARATIVE_NODES.md.
|
|
15
15
|
*/
|
|
16
16
|
import {
|
|
17
17
|
getNodeRegistry,
|
|
@@ -85,7 +85,7 @@ export interface RunContext {
|
|
|
85
85
|
* its own address. Loopback, because :4106 is the ungated internal listener and is published to
|
|
86
86
|
* 127.0.0.1 only.
|
|
87
87
|
*/
|
|
88
|
-
scope: { userId?: string; workflowId?: string; conversationId?: string; chatId?: string; executionId?: string; nodeId?: string; platformUrl?: string; engineUrl?: string };
|
|
88
|
+
scope: { userId?: string; workflowId?: string; org?: string; conversationId?: string; chatId?: string; executionId?: string; nodeId?: string; platformUrl?: string; engineUrl?: string };
|
|
89
89
|
/**
|
|
90
90
|
* THE CONVERSATION SO FAR, for a tool loop against a vendor with no chain id.
|
|
91
91
|
*
|
|
@@ -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";
|
|
@@ -54,4 +54,4 @@ The two Vite warnings a developer sees when starting Studio come from `discovery
|
|
|
54
54
|
`loader.ts` (dynamic `import()` of a package by name). They are correct, they are
|
|
55
55
|
unfixable while this folder exists, and they leave when it does.
|
|
56
56
|
|
|
57
|
-
See `docs/architecture/DECLARATIVE_NODES.md` §7a and §10.
|
|
57
|
+
See `docs/architecture/authoring/DECLARATIVE_NODES.md` §7a and §10.
|
package/vendor/sdk/appEngine.tsx
CHANGED
|
@@ -274,7 +274,7 @@ export function AppEngine(cfg: AppConfig) {
|
|
|
274
274
|
// — nothing to write.
|
|
275
275
|
const declared = parsed?.manifest?.defaultState ?? parsed?.manifest?.mode ?? parsed?.defaultState ?? parsed?.mode;
|
|
276
276
|
if (declared && declared !== "app") store.mergeAppState({ defaultState: declared });
|
|
277
|
-
conn.trigger({ app, metadata: { targetTriggerNode: b.trigger } });
|
|
277
|
+
conn.trigger({ app, metadata: { targetTriggerNode: b.trigger, ...(def?.org ? { org: def.org } : {}) } });
|
|
278
278
|
})
|
|
279
279
|
.catch((e: any) => console.error("[app] launch failed:", e));
|
|
280
280
|
return;
|
package/vite.config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Local Studio — the standalone developer tool (docs/architecture/LOCAL_STUDIO.md).
|
|
2
|
+
* Local Studio — the standalone developer tool (docs/architecture/authoring/LOCAL_STUDIO.md).
|
|
3
3
|
*
|
|
4
4
|
* Runs entirely offline against the developer's own project folder. No Postgres, no
|
|
5
5
|
* Redis, no engine, no Docker: the catalog routes are served straight off disk by the
|