ygl-trees 0.66.0 → 0.68.0
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/CHANGELOG.md +15 -0
- package/dist/apps/cli/src/bin.js +165 -130
- package/dist/apps/cli/src/profile-boot.js +11 -3
- package/dist/bundle/bin.cjs +105 -36
- package/dist/packages/core/database/src/db.js +421 -379
- package/dist/packages/core/database/src/plugin.js +5 -3
- package/dist/packages/core/database/src/types.js +8 -2
- package/dist/packages/ops/tools/src/doctor.js +19 -7
- package/dist/packages/ops/tools/src/plugins.js +10 -2
- package/dist/packages/research/pipeline/src/entities.js +6 -0
- package/dist/packages/research/pipeline/src/writes.js +1 -1
- package/dist/packages/techtree/model/src/writes.js +14 -14
- package/package.json +1 -1
|
@@ -17,9 +17,11 @@ class DatabaseService extends cordis_1.Service {
|
|
|
17
17
|
// GPT-6.1 SOL fix: the DB lock was only applied in the profile-boot
|
|
18
18
|
// fallback path, but the base bundle always instantiates this Service —
|
|
19
19
|
// the primary path never locked. Lock here (the actual entry point for
|
|
20
|
-
// every CLI invocation), preserving
|
|
21
|
-
// test execution
|
|
22
|
-
|
|
20
|
+
// every CLI invocation), preserving bypasses:
|
|
21
|
+
// YGL_TEST=1 — parallel test execution
|
|
22
|
+
// YGL_SERVE=1 — webui serve (long-lived; WAL permits concurrent access)
|
|
23
|
+
const lock = process.env.YGL_TEST !== "1" && process.env.YGL_SERVE !== "1";
|
|
24
|
+
this.db = new db_1.DB(dbFile, { lock });
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
exports.DatabaseService = DatabaseService;
|
|
@@ -14,8 +14,14 @@ function parse(argv) {
|
|
|
14
14
|
const flags = {};
|
|
15
15
|
let rest = false;
|
|
16
16
|
const takesValue = (name) => !BOOLEAN_FLAGS.has(name);
|
|
17
|
-
/**
|
|
18
|
-
|
|
17
|
+
/** A token is a flag boundary only if this parser could itself consume it as
|
|
18
|
+
* a flag: `--`, a long `--name`, or a single-letter `-x`. Any other token
|
|
19
|
+
* starting with `-` (e.g. `- 双平台…`, `-1`, `-abc`) can never be parsed as
|
|
20
|
+
* a flag, so a value-taking flag safely claims it — `--quote "- text"`
|
|
21
|
+
* keeps its value instead of collapsing to `true` and leaking the text
|
|
22
|
+
* into positionals. */
|
|
23
|
+
const looksLikeFlag = (s) => s === "--" || s.startsWith("--") || /^-[A-Za-z]$/.test(s);
|
|
24
|
+
const isValue = (s) => !looksLikeFlag(s);
|
|
19
25
|
for (let i = 0; i < argv.length; i++) {
|
|
20
26
|
const a = argv[i];
|
|
21
27
|
if (rest) {
|
|
@@ -137,9 +137,16 @@ function run(env, action, rest, args) {
|
|
|
137
137
|
});
|
|
138
138
|
}
|
|
139
139
|
// ── 7. Stale lock check ──
|
|
140
|
+
// The CLI process running doctor holds its own .ygllock while checking —
|
|
141
|
+
// exclude our own live lock; warn only on foreign/stale holders.
|
|
140
142
|
const dbPath = db.path;
|
|
141
|
-
const lockPath = `${dbPath}.
|
|
142
|
-
|
|
143
|
+
const lockPath = `${dbPath}.ygllock`;
|
|
144
|
+
let lockHolder = "";
|
|
145
|
+
try {
|
|
146
|
+
lockHolder = require("node:fs").readFileSync(`${lockPath}/pid`, "utf8").trim();
|
|
147
|
+
}
|
|
148
|
+
catch { /* absent/unreadable */ }
|
|
149
|
+
const hasLock = require("node:fs").existsSync(lockPath) && lockHolder !== String(process.pid);
|
|
143
150
|
checks.push({
|
|
144
151
|
name: "stale_lock",
|
|
145
152
|
status: hasLock ? "warn" : "pass",
|
|
@@ -151,8 +158,8 @@ function run(env, action, rest, args) {
|
|
|
151
158
|
// A malformed envelope silently degrades: evalChoice/evalScore fall back to
|
|
152
159
|
// empty options/levels → the gate can never resolve (always pending).
|
|
153
160
|
{
|
|
154
|
-
const evalConds = db.all(`SELECT c.id, c.name, c.op, c.value, n.key AS node
|
|
155
|
-
FROM node_conditions c JOIN nodes n ON n.id=c.node_id
|
|
161
|
+
const evalConds = db.all(`SELECT c.id, c.name, c.op, c.value, n.key AS node
|
|
162
|
+
FROM node_conditions c JOIN nodes n ON n.id=c.node_id
|
|
156
163
|
WHERE c.op IN ('eval','eval-choice','eval-score')`);
|
|
157
164
|
const bad = [];
|
|
158
165
|
for (const c of evalConds) {
|
|
@@ -240,9 +247,14 @@ function run(env, action, rest, args) {
|
|
|
240
247
|
db.run("UPDATE meta SET value='0' WHERE key='index_dirty'");
|
|
241
248
|
fixes.push("index dirty flag cleared (run: ygl-trees node index)");
|
|
242
249
|
}
|
|
243
|
-
// stale lock → remove
|
|
244
|
-
const lockDir = `${env.db.path}.
|
|
245
|
-
|
|
250
|
+
// stale lock → remove (never our own live lock)
|
|
251
|
+
const lockDir = `${env.db.path}.ygllock`;
|
|
252
|
+
let holder = "";
|
|
253
|
+
try {
|
|
254
|
+
holder = fs.readFileSync(`${lockDir}/pid`, "utf8").trim();
|
|
255
|
+
}
|
|
256
|
+
catch { /* */ }
|
|
257
|
+
if (fs.existsSync(lockDir) && holder !== String(process.pid)) {
|
|
246
258
|
try {
|
|
247
259
|
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
248
260
|
fixes.push("stale lock removed");
|
|
@@ -152,14 +152,22 @@ function pnpm(home, args) {
|
|
|
152
152
|
// embedded/offline carriers without touching this module. PATH may carry
|
|
153
153
|
// broken .bin shims (observed: a parent node_modules shim pointing at a
|
|
154
154
|
// missing pnpm.cjs under vitest) — resolve by probing, not first-hit.
|
|
155
|
+
//
|
|
156
|
+
// --ignore-workspace: pnpm walks UP from --dir to the nearest
|
|
157
|
+
// pnpm-workspace.yaml. When the profile lives inside a workspace (the unit
|
|
158
|
+
// tests' tmp/ profiles — or any repo-embedded home), the profile registered
|
|
159
|
+
// as a workspace importer and leaked tmp/plugins-unit-* blocks into the
|
|
160
|
+
// repo lockfile (committed that way through 0.66.0). Standalone profiles
|
|
161
|
+
// (the real $YGL_HOME) are outside any workspace, so this flag is a no-op
|
|
162
|
+
// there. Requires pnpm >= 9.4.
|
|
155
163
|
const pm = process.env.YGL_PACKAGE_MANAGER || "pnpm";
|
|
156
164
|
if (process.platform === "win32") {
|
|
157
165
|
const q = (s) => `"${String(s).replace(/["&|<>^]/g, "")}"`;
|
|
158
166
|
const exe = resolvePm(pm);
|
|
159
|
-
(0, node_child_process_1.execFileSync)([q(exe), ...args.map(q), "--dir", q(profileDir(home))].join(" "), { stdio: "pipe", shell: true });
|
|
167
|
+
(0, node_child_process_1.execFileSync)([q(exe), ...args.map(q), "--dir", q(profileDir(home)), "--ignore-workspace"].join(" "), { stdio: "pipe", shell: true });
|
|
160
168
|
return;
|
|
161
169
|
}
|
|
162
|
-
(0, node_child_process_1.execFileSync)(pm, [...args, "--dir", profileDir(home)], { stdio: "pipe" });
|
|
170
|
+
(0, node_child_process_1.execFileSync)(pm, [...args, "--dir", profileDir(home), "--ignore-workspace"], { stdio: "pipe" });
|
|
163
171
|
}
|
|
164
172
|
let cachedPm;
|
|
165
173
|
function resolvePm(pm) {
|
|
@@ -338,6 +338,11 @@ let Claim = class Claim {
|
|
|
338
338
|
contentHash;
|
|
339
339
|
created;
|
|
340
340
|
updated;
|
|
341
|
+
// The @ManyToOne here was missing (likely lost in an early edit): Claim.project
|
|
342
|
+
// was an undecorated property, so Project.claims' inverseSide never resolved
|
|
343
|
+
// and every ORM find loading the claims relation threw
|
|
344
|
+
// "Cannot read properties of undefined (reading 'joinColumns')" —
|
|
345
|
+
// `project show` / demo have been broken on this since the TypeORM pilot.
|
|
341
346
|
project;
|
|
342
347
|
claimEvidence;
|
|
343
348
|
};
|
|
@@ -375,6 +380,7 @@ __decorate([
|
|
|
375
380
|
__metadata("design:type", String)
|
|
376
381
|
], Claim.prototype, "updated", void 0);
|
|
377
382
|
__decorate([
|
|
383
|
+
(0, typeorm_1.ManyToOne)(() => Project, (p) => p.claims, { createForeignKeyConstraints: false }),
|
|
378
384
|
(0, typeorm_1.JoinColumn)({ name: "project_id" }),
|
|
379
385
|
__metadata("design:type", Project)
|
|
380
386
|
], Claim.prototype, "project", void 0);
|
|
@@ -344,7 +344,7 @@ async function taskSetStatus(ds, taskId, status) {
|
|
|
344
344
|
}
|
|
345
345
|
catch { /* malformed → reset */ }
|
|
346
346
|
const from = meta.status || "todo";
|
|
347
|
-
await mgr.query(`UPDATE artifacts SET meta=? WHERE id=?`, [JSON.stringify({ ...meta, status }), taskId]);
|
|
347
|
+
await mgr.query(`UPDATE artifacts SET meta=?, updated=? WHERE id=?`, [JSON.stringify({ ...meta, status }), (0, text_1.nowIso)(), taskId]);
|
|
348
348
|
await logEvent(mgr, "task.status", "task", taskId, `${from} → ${status}`, status === "done" ? "info" : "info");
|
|
349
349
|
return { id: taskId, title: rows[0].title, from, to: status };
|
|
350
350
|
});
|
|
@@ -134,8 +134,8 @@ async function buildIndex(mgr, treeId) {
|
|
|
134
134
|
inDegree[n.key] = (requiredParents.get(n.id) || []).length;
|
|
135
135
|
for (const n of nodes)
|
|
136
136
|
await mgr.query(`UPDATE nodes SET tier=? WHERE id=? AND tier<>?`, [tierOf.get(n.id) ?? 0, n.id, tierOf.get(n.id) ?? 0]);
|
|
137
|
-
await mgr.query(`INSERT INTO node_index(tree_id,topo_order,tiers,in_degree,built_at,dirty) VALUES(?,?,?,?,?,0)
|
|
138
|
-
ON CONFLICT(tree_id) DO UPDATE SET topo_order=excluded.topo_order, tiers=excluded.tiers,
|
|
137
|
+
await mgr.query(`INSERT INTO node_index(tree_id,topo_order,tiers,in_degree,built_at,dirty) VALUES(?,?,?,?,?,0)
|
|
138
|
+
ON CONFLICT(tree_id) DO UPDATE SET topo_order=excluded.topo_order, tiers=excluded.tiers,
|
|
139
139
|
in_degree=excluded.in_degree, built_at=excluded.built_at, dirty=0`, [treeId, JSON.stringify(topo), JSON.stringify(tiers), JSON.stringify(inDegree), (0, text_1.nowIso)()]);
|
|
140
140
|
return { topo, tiers, inDegree };
|
|
141
141
|
}
|
|
@@ -222,10 +222,10 @@ async function link(ds, treeId, parentKey, childKey, o = {}) {
|
|
|
222
222
|
const before = (await mgr.query(`SELECT id FROM node_edges WHERE parent_id=? AND child_id=? AND rel=?`, [p[0].id, c[0].id, rel]));
|
|
223
223
|
const duplicate = before.length > 0;
|
|
224
224
|
const id = duplicate ? before[0].id : (0, text_1.shortId)(["edge", p[0].id, c[0].id, rel], 12);
|
|
225
|
-
await mgr.query(`INSERT INTO node_edges(id,tree_id,parent_id,child_id,rel,mode,required,min_level,weight,note,created)
|
|
226
|
-
VALUES(?,?,?,?,?,?,?,?,?,?,?)
|
|
227
|
-
ON CONFLICT(parent_id,child_id,rel) DO UPDATE SET
|
|
228
|
-
mode=excluded.mode, required=excluded.required, min_level=excluded.min_level,
|
|
225
|
+
await mgr.query(`INSERT INTO node_edges(id,tree_id,parent_id,child_id,rel,mode,required,min_level,weight,note,created)
|
|
226
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?)
|
|
227
|
+
ON CONFLICT(parent_id,child_id,rel) DO UPDATE SET
|
|
228
|
+
mode=excluded.mode, required=excluded.required, min_level=excluded.min_level,
|
|
229
229
|
weight=excluded.weight, note=excluded.note`, [id, treeId, p[0].id, c[0].id, rel, mode, o.required === false ? 0 : 1, o.min_level ?? 1, o.weight ?? 1, o.note || "", (0, text_1.nowIso)()]);
|
|
230
230
|
await rebuildClosure(mgr, treeId);
|
|
231
231
|
await logEvent(mgr, "node.link", id, { tree: treeId, parent: parentKey, child: childKey, rel, mode, required: o.required !== false, duplicate });
|
|
@@ -256,7 +256,7 @@ async function condAdd(ds, treeId, keyOrId, o) {
|
|
|
256
256
|
if (!db_1.COND_KINDS.includes(kind))
|
|
257
257
|
throw new Error(`kind must be one of ${db_1.COND_KINDS.join("|")}`);
|
|
258
258
|
const id = (0, text_1.shortId)(["cond", n[0].id, o.name], 12);
|
|
259
|
-
await mgr.query(`INSERT INTO node_conditions(id,node_id,name,kind,expr,op,value,required,effect,weight,note)
|
|
259
|
+
await mgr.query(`INSERT INTO node_conditions(id,node_id,name,kind,expr,op,value,required,effect,weight,note)
|
|
260
260
|
VALUES(?,?,?,?,?,?,?,?,?,?,?)`, [id, n[0].id, o.name, kind, o.expr || o.name, o.op || ">=", o.value || "", o.required === false ? 0 : 1, o.effect || "gate", o.weight ?? 1, o.note || ""]);
|
|
261
261
|
await markDirty(mgr, treeId);
|
|
262
262
|
await logEvent(mgr, "node.cond", id, { tree: treeId, node: keyOrId, name: o.name, kind, op: o.op || ">=", required: o.required !== false });
|
|
@@ -269,7 +269,7 @@ async function stateSet(ds, treeId, keyOrId, status, context = "default", level
|
|
|
269
269
|
const n = (await mgr.query(`SELECT id FROM nodes WHERE tree_id=? AND (key=? OR id=?)`, [treeId, keyOrId, keyOrId]));
|
|
270
270
|
if (!n.length)
|
|
271
271
|
throw new Error(`node not found: ${keyOrId}`);
|
|
272
|
-
await mgr.query(`INSERT INTO node_state(tree_id,context,node_id,status,level,progress,updated) VALUES(?,?,?,?,?,?,?)
|
|
272
|
+
await mgr.query(`INSERT INTO node_state(tree_id,context,node_id,status,level,progress,updated) VALUES(?,?,?,?,?,?,?)
|
|
273
273
|
ON CONFLICT(tree_id,context,node_id) DO UPDATE SET status=excluded.status, level=excluded.level, updated=excluded.updated`, [treeId, context, n[0].id, status, level, status === "unlocked" ? 1 : 0, (0, text_1.nowIso)()]);
|
|
274
274
|
// R10.1: unlock/lock are the timeline anchors for R11's time axis
|
|
275
275
|
await logEvent(mgr, status === "unlocked" ? "node.unlock" : "node.lock", n[0].id, {
|
|
@@ -414,20 +414,20 @@ async function importTree(ds, spec, opts = {}) {
|
|
|
414
414
|
for (const e of edges) {
|
|
415
415
|
const pid = nodeId.get(String(e.parent));
|
|
416
416
|
const cid = nodeId.get(String(e.child));
|
|
417
|
-
await mgr.query(`INSERT INTO node_edges(id,tree_id,parent_id,child_id,rel,mode,required,min_level,weight,note,created)
|
|
418
|
-
VALUES(
|
|
419
|
-
ON CONFLICT(parent_id,child_id,rel) DO UPDATE SET
|
|
420
|
-
mode=excluded.mode, required=excluded.required, min_level=excluded.min_level,
|
|
417
|
+
await mgr.query(`INSERT INTO node_edges(id,tree_id,parent_id,child_id,rel,mode,required,min_level,weight,note,created)
|
|
418
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?)
|
|
419
|
+
ON CONFLICT(parent_id,child_id,rel) DO UPDATE SET
|
|
420
|
+
mode=excluded.mode, required=excluded.required, min_level=excluded.min_level,
|
|
421
421
|
weight=excluded.weight, note=excluded.note`, [
|
|
422
422
|
(0, text_1.shortId)(["edge", tid, pid, cid, String(e.rel)], 12), tid, pid, cid,
|
|
423
423
|
String(e.rel), String(e.mode || "AND"), e.required === false ? 0 : 1,
|
|
424
|
-
Number(e.min_level ?? 1), Number(e.weight ?? 1), String(e.note || ""),
|
|
424
|
+
Number(e.min_level ?? 1), Number(e.weight ?? 1), String(e.note || ""), (0, text_1.nowIso)(),
|
|
425
425
|
]);
|
|
426
426
|
}
|
|
427
427
|
for (const c of conds) {
|
|
428
428
|
const nid = nodeId.get(String(c.node));
|
|
429
429
|
await mgr.query(`DELETE FROM node_conditions WHERE node_id=? AND name=?`, [nid, String(c.name)]);
|
|
430
|
-
await mgr.query(`INSERT INTO node_conditions(id,node_id,name,kind,expr,op,value,required,effect,weight,note)
|
|
430
|
+
await mgr.query(`INSERT INTO node_conditions(id,node_id,name,kind,expr,op,value,required,effect,weight,note)
|
|
431
431
|
VALUES(?,?,?,?,?,?,?,?,?,?,?)`, [
|
|
432
432
|
(0, text_1.shortId)(["cond", tid, nid, String(c.name)], 10), nid, String(c.name),
|
|
433
433
|
String(c.kind || "flag"), String(c.expr || ""), String(c.op || ">="), String(c.value ?? ""),
|
package/package.json
CHANGED