unoverse 0.1.173 → 0.1.175
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.
|
@@ -108,6 +108,7 @@ cmd_db_verify() {
|
|
|
108
108
|
dictionary_regions: [
|
|
109
109
|
"region_id", "workflow_id", "depth", "name", "description", "name_locked",
|
|
110
110
|
"locked_at", "locked_by", "stage", "stage_set_by", "needs_review",
|
|
111
|
+
"skills", "skills_set_by", "skills_set_at",
|
|
111
112
|
"derived_from", "first_seen", "last_seen", "closed_at"
|
|
112
113
|
],
|
|
113
114
|
dictionary_content_chunks: [
|
package/package.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* performs correctly, and a half-published project is worse than either extreme.
|
|
19
19
|
*/
|
|
20
20
|
import { readdirSync, readFileSync, existsSync, statSync } from "node:fs";
|
|
21
|
-
import { join, relative } from "node:path";
|
|
21
|
+
import { join, relative, basename } from "node:path";
|
|
22
22
|
import { fingerprintOf } from "./fingerprint.js";
|
|
23
23
|
import { designSystemVersion } from "./baseVersion.js";
|
|
24
24
|
/** Folders under rx/ that are never a developer's own project. */
|
|
@@ -87,15 +87,18 @@ export function collectProject(designRoot, project) {
|
|
|
87
87
|
// could disagree with itself mid-run if a file changed.
|
|
88
88
|
const base_version = designSystemVersion(designSystemDir(designRoot));
|
|
89
89
|
const items = [];
|
|
90
|
-
// A COMPONENT row's name is the qualified ref
|
|
91
|
-
// identity is (kind, name) with no org in the
|
|
92
|
-
// only WITHIN an org — two orgs may ship
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
90
|
+
// A COMPONENT (and, since 2026-08-24, a SKILL) row's name is the qualified ref
|
|
91
|
+
// (`<org>/<name>`): the items table's identity is (kind, name) with no org in the
|
|
92
|
+
// key, and both names are unique only WITHIN an org — two orgs may ship
|
|
93
|
+
// `course-card` or `hr-education-coach`, so the org must be in the name for the
|
|
94
|
+
// rows to coexist (the same trick a style row already uses: its name IS the org).
|
|
95
|
+
// A prompt block is NEVER qualified this way — see the blocks/ walk below for why.
|
|
96
|
+
// Templates keep their bare ids: those are org-qualified by convention
|
|
97
|
+
// (`<org>-chat-layout`). docs/unoverse/UNOVERSE_COMPONENT_ORGS.md.
|
|
98
|
+
const QUALIFIED_KINDS = new Set(["component", "skill"]);
|
|
96
99
|
const add = (kind, name, definition) => items.push({
|
|
97
100
|
kind,
|
|
98
|
-
name: kind
|
|
101
|
+
name: QUALIFIED_KINDS.has(kind) ? `${project}/${name}` : name,
|
|
99
102
|
definition,
|
|
100
103
|
fingerprint: fingerprintOf(definition),
|
|
101
104
|
org: project,
|
|
@@ -135,6 +138,53 @@ export function collectProject(designRoot, project) {
|
|
|
135
138
|
const styles = filesUnder(join(root, "styles"));
|
|
136
139
|
if (Object.keys(styles).length)
|
|
137
140
|
add("style", project, { files: styles });
|
|
141
|
+
// skills/ — ORG-QUALIFIED, same trick as a component: `${project}/${name}` is the
|
|
142
|
+
// identity, so two orgs may each ship "hr-education-coach" without one silently
|
|
143
|
+
// taking the other's row (items/loaders.ts loadParsedSkill resolves the qualified
|
|
144
|
+
// form back to this same folder). One item per skill folder, same shape as a
|
|
145
|
+
// component: SKILL.md plus any references/ it carries.
|
|
146
|
+
const skillsHome = join(root, "skills");
|
|
147
|
+
if (existsSync(skillsHome)) {
|
|
148
|
+
for (const e of readdirSync(skillsHome, { withFileTypes: true })) {
|
|
149
|
+
if (!e.isDirectory() || e.name.startsWith("."))
|
|
150
|
+
continue;
|
|
151
|
+
if (!existsSync(join(skillsHome, e.name, "SKILL.md")))
|
|
152
|
+
continue; // not a skill folder
|
|
153
|
+
const files = filesUnder(join(skillsHome, e.name));
|
|
154
|
+
if (Object.keys(files).length)
|
|
155
|
+
add("skill", e.name, { files });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// blocks/ — NAME STAYS BARE, unlike a skill. A block is referenced inline in authored
|
|
159
|
+
// component text as `{{prompt.name}}` (packages'/engine's template resolver), a single
|
|
160
|
+
// FLAT name across the whole platform with no room for an org segment. Org-qualifying
|
|
161
|
+
// it would break every existing `{{prompt.X}}` reference, so only where the block
|
|
162
|
+
// lives moves; what it is called does not. Two orgs shipping the same block name still
|
|
163
|
+
// collide at the row (last publish wins) — an accepted limit, not solved here.
|
|
164
|
+
const blocksHome = join(root, "blocks");
|
|
165
|
+
if (existsSync(blocksHome)) {
|
|
166
|
+
const walk = (d) => {
|
|
167
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
168
|
+
if (e.name.startsWith("."))
|
|
169
|
+
continue;
|
|
170
|
+
const p = join(d, e.name);
|
|
171
|
+
if (e.isDirectory())
|
|
172
|
+
walk(p);
|
|
173
|
+
else if (e.name.endsWith(".md")) {
|
|
174
|
+
const definition = { files: { [relative(blocksHome, p)]: readFileSync(p, "utf8") } };
|
|
175
|
+
items.push({
|
|
176
|
+
kind: "prompt-block",
|
|
177
|
+
name: basename(e.name, ".md"),
|
|
178
|
+
definition,
|
|
179
|
+
fingerprint: fingerprintOf(definition),
|
|
180
|
+
org: project,
|
|
181
|
+
base_version,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
walk(blocksHome);
|
|
187
|
+
}
|
|
138
188
|
return items;
|
|
139
189
|
}
|
|
140
190
|
//# sourceMappingURL=collect.js.map
|
|
@@ -59,6 +59,16 @@ project = "", fetchImpl = fetch) {
|
|
|
59
59
|
* the project (a fresh clone missing its files, the wrong folder entirely) does not
|
|
60
60
|
* get to empty the universe. Removals are proposed only when the workspace holds at
|
|
61
61
|
* least one item — deleting the last one is done by name, deliberately.
|
|
62
|
+
*
|
|
63
|
+
* AND A SECOND GUARD, ON KINDS. Absence only means "deleted" for a kind this
|
|
64
|
+
* collector actually LOOKED for. A CLI that predates a kind collects none of it, sees
|
|
65
|
+
* rows it cannot account for, and proposes deleting every one — which is exactly what
|
|
66
|
+
* happened live on 2026-08-24: an older global `unoverse` removed all three freshly
|
|
67
|
+
* published skill and prompt-block rows, because its collector had never heard of
|
|
68
|
+
* them. Version skew between a machine's CLI and its universe is normal and must not
|
|
69
|
+
* be destructive, so a kind that contributed NOTHING to this collection is left
|
|
70
|
+
* entirely alone. Deleting the last item OF A KIND is therefore done by name, the
|
|
71
|
+
* same deliberate act as emptying a project.
|
|
62
72
|
*/
|
|
63
73
|
if (project && items.length > 0) {
|
|
64
74
|
try {
|
|
@@ -70,7 +80,10 @@ project = "", fetchImpl = fetch) {
|
|
|
70
80
|
if (res.ok) {
|
|
71
81
|
const doc = (await res.json().catch(() => null));
|
|
72
82
|
const have = new Set(items.map((i) => `${i.kind}/${i.name}`));
|
|
83
|
+
const collectedKinds = new Set(items.map((i) => i.kind));
|
|
73
84
|
for (const r of doc?.items ?? []) {
|
|
85
|
+
if (!collectedKinds.has(r.kind))
|
|
86
|
+
continue; // this collector never looked for it
|
|
74
87
|
if (!have.has(`${r.kind}/${r.name}`))
|
|
75
88
|
plan.remove.push(r);
|
|
76
89
|
}
|
|
@@ -34,7 +34,7 @@ function checkStateOrder(order, rootFolder, file, includeLayouts = false) {
|
|
|
34
34
|
const onDisk = new Set([...stateNames, ...(includeLayouts ? dirNames("layouts") : [])]);
|
|
35
35
|
for (const name of order)
|
|
36
36
|
if (typeof name === "string" && !onDisk.has(name))
|
|
37
|
-
report("error", file, `stateOrder lists "${name}" but no states/${name}${includeLayouts ? ` or layouts/${name}` : ""} definition exists (docs/design/${includeLayouts ? "
|
|
37
|
+
report("error", file, `stateOrder lists "${name}" but no states/${name}${includeLayouts ? ` or layouts/${name}` : ""} definition exists (docs.unoverse.ai/design/${includeLayouts ? "apps" : "components"})`);
|
|
38
38
|
// Only STATES must appear in stateOrder to lock the picker order; the default layout is
|
|
39
39
|
// legitimately omitted, so never warn on layouts.
|
|
40
40
|
for (const name of stateNames)
|
|
@@ -488,6 +488,14 @@ function lintFile(file) {
|
|
|
488
488
|
if (typeof arrival !== "string")
|
|
489
489
|
report("error", file, `a faced component must declare its base state — a v2 state.view tree \`initial\`, or (legacy) manifest.defaultState / state.defaultState (docs.unoverse.ai/design/components)`);
|
|
490
490
|
}
|
|
491
|
+
// A tree DECLARES its order, so an authored stateOrder beside one is a second source
|
|
492
|
+
// of truth that can silently disagree with it. Five docs call this form legacy and
|
|
493
|
+
// nothing enforced it, so a component kept running on one and never heard a word.
|
|
494
|
+
// The old check only fired when a states/ FOLDER existed, which a fragment-based
|
|
495
|
+
// component does not have (docs/doc-control §Guards: enforce rules as tests).
|
|
496
|
+
if (viewTree && json.stateOrder !== undefined)
|
|
497
|
+
report("error", file, `authored "stateOrder" beside a "state.view" tree. The tree declares the order: nest these as substates and delete the list (docs.unoverse.ai/design/state)`);
|
|
498
|
+
|
|
491
499
|
if (stateFiles.length) {
|
|
492
500
|
const order = Array.isArray(json.stateOrder) ? [...json.stateOrder].sort() : null;
|
|
493
501
|
if (!order || !order.length)
|