@unoverse-platform/studio 0.1.96 → 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.
Files changed (40) hide show
  1. package/bin/studio.mjs +1 -1
  2. package/local/authoring.ts +1 -1
  3. package/local/catalog.ts +17 -1
  4. package/local/keys.ts +1 -1
  5. package/local/nodes.ts +1 -1
  6. package/local/plugin.ts +6 -2
  7. package/local/sample-identity/brand/brand.yaml +88 -0
  8. package/local/sample-identity/brand/manifest.yaml +13 -0
  9. package/local/sample-identity/organisation/manifest.yaml +15 -0
  10. package/local/sample-identity/organisation/organisation.yaml +53 -0
  11. package/local/sample-identity/purpose/manifest.yaml +12 -0
  12. package/local/sample-identity/purpose/purpose.yaml +46 -0
  13. package/local/sample-identity/story/manifest.yaml +12 -0
  14. package/local/sample-identity/story/story.yaml +63 -0
  15. package/local/scaffold.mjs +12 -3
  16. package/local/source.ts +1 -1
  17. package/package.json +1 -1
  18. package/screens/IdentityHeader.tsx +1 -1
  19. package/screens/IdentityView.tsx +206 -0
  20. package/screens/SourceView.tsx +1 -1
  21. package/screens/index.ts +3 -2
  22. package/src/App.tsx +7 -2
  23. package/src/NodeKeys.tsx +1 -1
  24. package/src/host.ts +3 -3
  25. package/vendor/base/src/agent-mcp/memoryIngest.ts +1 -1
  26. package/vendor/base/src/definitions/registry/identitySeed.ts +17 -0
  27. package/vendor/base/src/definitions/registry/orgDocuments.ts +67 -0
  28. package/vendor/base/src/definitions/registry/orgPack.ts +50 -0
  29. package/vendor/base/src/items/collect.ts +6 -2
  30. package/vendor/base/src/items/publish.ts +1 -1
  31. package/vendor/base/src/lint/design/vocabulary.mjs +1 -1
  32. package/vendor/base/src/lint/nodes/index.mjs +1 -1
  33. package/vendor/base/src/manifests/compose.ts +1 -1
  34. package/vendor/base/src/manifests/executor/context.ts +3 -0
  35. package/vendor/base/src/manifests/index.ts +1 -1
  36. package/vendor/base/src/manifests/runtime/context.ts +1 -1
  37. package/vendor/base/src/manifests/source.ts +1 -1
  38. package/vendor/base/src/plugins/README.md +1 -1
  39. package/vendor/sdk/appEngine.tsx +1 -1
  40. 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";
@@ -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: ""
@@ -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 three authoring folders and one component.
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.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unoverse-platform/studio",
3
- "version": "0.1.96",
3
+ "version": "0.1.97",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {
@@ -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-r py-1.5 pl-3" style={{ borderLeft: `3px solid ${accent}`, background: "#f3f4f6" }}>
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
+ }
@@ -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
- const QUALIFIED_KINDS = new Set(["component", "skill", "template"]);
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
- for (const [dir, kind] of [["components", "component"], ["apps", "app"], ["templates", "template"], ["atoms", "atom"]] as const) {
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", "actionPrompt", "primaryImage", "images", "action",
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
@@ -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.
@@ -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