unoverse 0.1.174 → 0.1.176
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.
|
@@ -116,13 +116,22 @@
|
|
|
116
116
|
# (scripts/lib/db-setup.sh). The old programmatic `require('./dist/db')` table
|
|
117
117
|
# setup is retired: the engine no longer ships a dist, and .sql migrations under
|
|
118
118
|
# apps/unoverse/engine/migrations are the single source of truth.
|
|
119
|
+
# --no-check-order, and it is NOT optional here: a database from before the
|
|
120
|
+
# 2026-07-28 squash has 002-018 recorded in pgmigrations with no matching files
|
|
121
|
+
# (they live inside 001_baseline now), so the order check refuses EVERY new
|
|
122
|
+
# migration with "Not run migration 019_… is preceding already run migration 002_…".
|
|
123
|
+
# db-setup.sh has carried this flag since the squash; this playbook runs the same
|
|
124
|
+
# command and did not, so `unoverse db-setup` worked while `unoverse deploy db`
|
|
125
|
+
# failed on the same database. Ordering still holds by filename and an applied name
|
|
126
|
+
# is never re-run, so the check buys nothing a squashed history can satisfy.
|
|
119
127
|
- name: "[4/5] Apply database migrations (node-pg-migrate)"
|
|
120
128
|
shell: |
|
|
121
129
|
docker compose exec -T -e NODE_TLS_REJECT_UNAUTHORIZED=0 unoverse \
|
|
122
130
|
npx node-pg-migrate up \
|
|
123
131
|
--migrations-dir /app/apps/unoverse/engine/migrations \
|
|
124
132
|
--migration-file-language sql \
|
|
125
|
-
--no-lock
|
|
133
|
+
--no-lock \
|
|
134
|
+
--no-check-order
|
|
126
135
|
args:
|
|
127
136
|
chdir: /opt/gravity
|
|
128
137
|
register: migrate_result
|
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
|
}
|