youiyoux 0.1.10 → 0.1.13
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/dist/chunks/{chunk-HDJ2TOWZ.js → chunk-5I2VHSEN.js} +1 -1
- package/dist/chunks/{chunk-T3KNPVQG.js → chunk-DX2NKY2M.js} +4 -4
- package/dist/chunks/{chunk-75DBKEAH.js → chunk-EV62TUDV.js} +1119 -619
- package/dist/chunks/{chunk-3RP664IP.js → chunk-N5Z5A637.js} +2 -2
- package/dist/chunks/{chunk-DWYDHVLZ.js → chunk-NF4OJYKC.js} +106 -66
- package/dist/chunks/{chunk-X2YYMHR4.js → chunk-PCLXMWZ2.js} +3 -3
- package/dist/chunks/{chunk-OCZJ3JGH.js → chunk-PWGZRP3C.js} +3 -3
- package/dist/chunks/{chunk-FLBRX6Y3.js → chunk-XOXF2ILO.js} +1 -1
- package/dist/chunks/{chunk-6GITMASZ.js → chunk-ZOQW6LNH.js} +56 -900
- package/dist/chunks/chunk-ZWVWENSB.js +899 -0
- package/dist/chunks/{src-XTNHB65M.js → src-4QOHR5GC.js} +3 -3
- package/dist/chunks/src-624TVB6T.js +55 -0
- package/dist/chunks/{src-DPFJ4S2N.js → src-E6ZPFKPR.js} +3 -2
- package/dist/chunks/{src-75ODGHE3.js → src-HVYMF4S2.js} +9 -7
- package/dist/chunks/{src-BYNWPNRT.js → src-IKDQ2BZO.js} +7 -1
- package/dist/chunks/{src-ZG7JU6YJ.js → src-PBM76LFQ.js} +2 -2
- package/dist/chunks/{src-6SNQVIZS.js → src-R7JGKOYI.js} +4 -4
- package/dist/chunks/{src-PURAHRCR.js → src-YC64NNQ3.js} +2 -2
- package/dist/hub/assets/{CandidatePanel-ZzbsyZo3.js → CandidatePanel-DfntneYT.js} +1 -1
- package/dist/hub/assets/{CatalogScreen-DDYlvOHy.js → CatalogScreen-kpFYZqX3.js} +1 -1
- package/dist/hub/assets/{ChangesScreen-C8_EyHgJ.js → ChangesScreen-Chu-Ui1B.js} +1 -1
- package/dist/hub/assets/{CheatSheet-DfcPiXdS.js → CheatSheet-B1dXTYTW.js} +1 -1
- package/dist/hub/assets/{CommandPalette-DfFkDTVg.js → CommandPalette-LBZHJ659.js} +1 -1
- package/dist/hub/assets/{DiscoverScreen-D6NTgEgk.js → DiscoverScreen-Yd3gVUG4.js} +2 -2
- package/dist/hub/assets/DraftEditor-QH9Sa_Uc.js +5 -0
- package/dist/hub/assets/{GapsScreen-CqedCj4z.js → GapsScreen-DPYGlNnA.js} +1 -1
- package/dist/hub/assets/{HomeScreen-CIj1QTkw.js → HomeScreen-BCPPk7mz.js} +1 -1
- package/dist/hub/assets/{InventoryScreen-CyvFXa2_.js → InventoryScreen-SleB733C.js} +1 -1
- package/dist/hub/assets/{LibraryScreen-C4WU0f42.js → LibraryScreen-BVXhOVPU.js} +1 -1
- package/dist/hub/assets/{Opened-CeRWkA7c.js → Opened-CJyMbxqV.js} +1 -1
- package/dist/hub/assets/OrgApp-Dxs_7CxZ.js +1 -0
- package/dist/hub/assets/{Preview-CgW-FMB7.js → Preview-DWFP0BIe.js} +1 -1
- package/dist/hub/assets/{RuleDetail-BDhZ1gCd.js → RuleDetail-C2fTlLJL.js} +3 -3
- package/dist/hub/assets/{RulesScreen-D3rSo_z2.js → RulesScreen-BABeAGJg.js} +3 -3
- package/dist/hub/assets/{ScreenImage-DLeA3nRh.js → ScreenImage-CyhKm_nj.js} +1 -1
- package/dist/hub/assets/ScreensScreen-8Ih8byGy.js +1 -0
- package/dist/hub/assets/{SearchField-DtEgq9-Q.js → SearchField-hF-6-YDM.js} +1 -1
- package/dist/hub/assets/{ServiceDown-Bn6AcA3J.js → ServiceDown-S4ya0Cob.js} +1 -1
- package/dist/hub/assets/{SpecimenCapture-p6YErF5y.js → SpecimenCapture-e_pgT4Sp.js} +1 -1
- package/dist/hub/assets/{Specimens-DuaE0WeO.js → Specimens-D_9hjlJH.js} +2 -2
- package/dist/hub/assets/{StackScreen-DHDulDng.js → StackScreen-J2FQilLW.js} +1 -1
- package/dist/hub/assets/{StubScreens-CmhVNX_Z.js → StubScreens-DAkMph3i.js} +1 -1
- package/dist/hub/assets/StyleScreen-C5WytSZU.js +1 -0
- package/dist/hub/assets/{Tabs-Cbh9BCKW.js → Tabs-DU-fFRDV.js} +1 -1
- package/dist/hub/assets/{TokenDetail-BICobLK1.js → TokenDetail-CDY1xxb9.js} +1 -1
- package/dist/hub/assets/{TokensScreen-CyTHn55q.js → TokensScreen-Sx4aICWk.js} +1 -1
- package/dist/hub/assets/{WorkspaceScreen-uPWqfklh.js → WorkspaceScreen-CzZ16fPZ.js} +1 -1
- package/dist/hub/assets/{client-BXHiuV_w.js → client-Bdgr12LP.js} +1 -1
- package/dist/hub/assets/{discover-CFhyKler.js → discover-va2KFuK_.js} +1 -1
- package/dist/hub/assets/{dist-DYNYuTjI.js → dist-C8tf4g8c.js} +1 -1
- package/dist/hub/assets/{dist-VmEpVQjP.js → dist-CC_p8DIj.js} +1 -1
- package/dist/hub/assets/{dist-C6iTWL0b.js → dist-YTWqg5sS.js} +1 -1
- package/dist/hub/assets/{family-tiles-B8_onXUi.js → family-tiles-qaK1-_c8.js} +1 -1
- package/dist/hub/assets/main-cvOFSjox.js +2 -0
- package/dist/hub/assets/main-pNY4iNRo.css +2 -0
- package/dist/hub/assets/{try-live-CX04d2L2.js → try-live-Iys3nCBG.js} +1 -1
- package/dist/hub/assets/when-COrv5lkZ.js +1 -0
- package/dist/hub/assets/{workflow-DbBdiOmA.js → workflow-C53UBmRZ.js} +1 -1
- package/dist/hub/index.html +2 -2
- package/dist/hub-previews.js +6 -5
- package/dist/hub-server.js +3 -3
- package/dist/hub-snapshot.js +1 -1
- package/dist/schema/youi.org.schema.json +438 -0
- package/dist/schema/youi.record.schema.json +338 -0
- package/dist/youi.js +7 -6
- package/dist/youiyoux.js +7 -6
- package/package.json +4 -4
- package/dist/hub/assets/DraftEditor-CUNGYbg0.js +0 -5
- package/dist/hub/assets/ScreensScreen-bGuFIyWj.js +0 -1
- package/dist/hub/assets/StyleScreen-LwTqfGID.js +0 -1
- package/dist/hub/assets/main-7snyIdoi.css +0 -2
- package/dist/hub/assets/main-CfbpuT4Y.js +0 -2
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
import { createRequire as __youiRequire } from "node:module"; import { dirname as __youiDir } from "node:path"; import { fileURLToPath as __youiPath } from "node:url"; const require = __youiRequire(import.meta.url); const __youiDirname = __youiDir(__youiPath(import.meta.url));
|
|
2
|
+
import {
|
|
3
|
+
SHAPE_DIMENSIONS,
|
|
4
|
+
shortHash
|
|
5
|
+
} from "./chunk-NF4OJYKC.js";
|
|
6
|
+
|
|
7
|
+
// ../store/src/driver.ts
|
|
8
|
+
function searchTerms(words2) {
|
|
9
|
+
const terms = words2.flatMap((w) => w.normalize("NFC").toLowerCase().split(/[^\p{L}\p{N}]+/u)).filter((w) => w.length > 0);
|
|
10
|
+
return [...new Set(terms)].slice(0, 64);
|
|
11
|
+
}
|
|
12
|
+
function numbered(sql) {
|
|
13
|
+
let n = 0;
|
|
14
|
+
return sql.replace(/\?/g, () => `$${++n}`);
|
|
15
|
+
}
|
|
16
|
+
var Lock = class {
|
|
17
|
+
tail = Promise.resolve();
|
|
18
|
+
run(fn) {
|
|
19
|
+
const next = this.tail.then(fn, fn);
|
|
20
|
+
this.tail = next.catch(() => {
|
|
21
|
+
});
|
|
22
|
+
return next;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
function pageInteger(value, fallback, max = 500) {
|
|
26
|
+
if (value === void 0 || Number.isNaN(value)) return fallback;
|
|
27
|
+
const bounded = Math.max(0, Math.min(max, Math.floor(value)));
|
|
28
|
+
return Number.isSafeInteger(bounded) ? bounded : fallback;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ../store/src/migrations.ts
|
|
32
|
+
var MIGRATIONS = [
|
|
33
|
+
{
|
|
34
|
+
id: 1,
|
|
35
|
+
name: "base",
|
|
36
|
+
statements: ({ json: json2 }) => [
|
|
37
|
+
`CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)`,
|
|
38
|
+
// Discover listings: metadata only, never source. scope 'chosen' is the synced sources, 'all' what --all added.
|
|
39
|
+
`CREATE TABLE IF NOT EXISTS discover_items (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
scope TEXT NOT NULL,
|
|
42
|
+
ref TEXT,
|
|
43
|
+
name TEXT NOT NULL,
|
|
44
|
+
title TEXT,
|
|
45
|
+
summary TEXT NOT NULL,
|
|
46
|
+
kind TEXT NOT NULL,
|
|
47
|
+
source_kind TEXT,
|
|
48
|
+
source_class TEXT NOT NULL,
|
|
49
|
+
registry TEXT,
|
|
50
|
+
licence TEXT NOT NULL,
|
|
51
|
+
style TEXT NOT NULL,
|
|
52
|
+
downloads INTEGER,
|
|
53
|
+
stars INTEGER,
|
|
54
|
+
archived INTEGER NOT NULL,
|
|
55
|
+
search_text TEXT NOT NULL,
|
|
56
|
+
entry ${json2} NOT NULL,
|
|
57
|
+
built_at TEXT NOT NULL
|
|
58
|
+
)`,
|
|
59
|
+
`CREATE INDEX IF NOT EXISTS discover_items_scope ON discover_items (scope, source_class)`,
|
|
60
|
+
`CREATE INDEX IF NOT EXISTS discover_items_ref ON discover_items (ref)`,
|
|
61
|
+
`CREATE TABLE IF NOT EXISTS discover_roles (item_id TEXT NOT NULL, role TEXT NOT NULL, PRIMARY KEY (item_id, role))`,
|
|
62
|
+
`CREATE INDEX IF NOT EXISTS discover_roles_role ON discover_roles (role)`,
|
|
63
|
+
`CREATE TABLE IF NOT EXISTS signals (key TEXT PRIMARY KEY, kind TEXT NOT NULL, data ${json2} NOT NULL, fetched_at TEXT)`,
|
|
64
|
+
`CREATE TABLE IF NOT EXISTS licences (item_id TEXT PRIMARY KEY, spdx TEXT NOT NULL, evidence_url TEXT, verified_at TEXT)`,
|
|
65
|
+
// Inventory history: one row per project per run.
|
|
66
|
+
`CREATE TABLE IF NOT EXISTS scans (
|
|
67
|
+
id TEXT PRIMARY KEY,
|
|
68
|
+
run TEXT NOT NULL,
|
|
69
|
+
source TEXT NOT NULL,
|
|
70
|
+
project TEXT NOT NULL,
|
|
71
|
+
root TEXT NOT NULL,
|
|
72
|
+
commit_sha TEXT,
|
|
73
|
+
scanned_at TEXT NOT NULL,
|
|
74
|
+
status TEXT NOT NULL,
|
|
75
|
+
files INTEGER NOT NULL,
|
|
76
|
+
looks INTEGER NOT NULL,
|
|
77
|
+
families INTEGER NOT NULL,
|
|
78
|
+
near_duplicates INTEGER NOT NULL,
|
|
79
|
+
roles ${json2} NOT NULL
|
|
80
|
+
)`,
|
|
81
|
+
`CREATE INDEX IF NOT EXISTS scans_project ON scans (project, scanned_at)`,
|
|
82
|
+
`CREATE INDEX IF NOT EXISTS scans_run ON scans (run)`,
|
|
83
|
+
`CREATE TABLE IF NOT EXISTS families (
|
|
84
|
+
run TEXT NOT NULL,
|
|
85
|
+
id TEXT NOT NULL,
|
|
86
|
+
role TEXT NOT NULL,
|
|
87
|
+
looks INTEGER NOT NULL,
|
|
88
|
+
near_duplicate INTEGER NOT NULL,
|
|
89
|
+
projects ${json2} NOT NULL,
|
|
90
|
+
kits ${json2} NOT NULL,
|
|
91
|
+
shape ${json2} NOT NULL,
|
|
92
|
+
PRIMARY KEY (run, id)
|
|
93
|
+
)`,
|
|
94
|
+
`CREATE TABLE IF NOT EXISTS looks (
|
|
95
|
+
run TEXT NOT NULL,
|
|
96
|
+
id TEXT NOT NULL,
|
|
97
|
+
project TEXT NOT NULL,
|
|
98
|
+
role TEXT NOT NULL,
|
|
99
|
+
file TEXT NOT NULL,
|
|
100
|
+
line INTEGER NOT NULL,
|
|
101
|
+
col INTEGER NOT NULL,
|
|
102
|
+
tag TEXT NOT NULL,
|
|
103
|
+
kit TEXT,
|
|
104
|
+
family TEXT,
|
|
105
|
+
routes ${json2} NOT NULL,
|
|
106
|
+
shape ${json2} NOT NULL,
|
|
107
|
+
PRIMARY KEY (run, id)
|
|
108
|
+
)`,
|
|
109
|
+
`CREATE INDEX IF NOT EXISTS looks_family ON looks (run, family)`,
|
|
110
|
+
// The graph: what uses what, read with recursive CTEs that are the same text on both engines.
|
|
111
|
+
`CREATE TABLE IF NOT EXISTS nodes (id TEXT PRIMARY KEY, kind TEXT NOT NULL, ref TEXT NOT NULL, source TEXT NOT NULL)`,
|
|
112
|
+
`CREATE INDEX IF NOT EXISTS nodes_ref ON nodes (kind, ref)`,
|
|
113
|
+
`CREATE TABLE IF NOT EXISTS edges ("from" TEXT NOT NULL, "to" TEXT NOT NULL, kind TEXT NOT NULL, source TEXT NOT NULL, PRIMARY KEY ("from", "to", kind))`,
|
|
114
|
+
`CREATE INDEX IF NOT EXISTS edges_to ON edges ("to", kind)`,
|
|
115
|
+
`CREATE INDEX IF NOT EXISTS edges_source ON edges (source)`
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
{ id: 2, name: "discover-search", statements: (dialect) => dialect.searchDdl },
|
|
119
|
+
{ id: 3, name: "project-graph", statements: () => [
|
|
120
|
+
`ALTER TABLE nodes ADD COLUMN project TEXT`,
|
|
121
|
+
`CREATE INDEX nodes_project ON nodes (project, kind, ref)`,
|
|
122
|
+
`CREATE INDEX looks_project_family ON looks (run, project, family)`,
|
|
123
|
+
// Old graph ids cannot be assigned safely to a project; rebuild restores this cache.
|
|
124
|
+
`DELETE FROM edges`,
|
|
125
|
+
`DELETE FROM nodes`
|
|
126
|
+
] },
|
|
127
|
+
// What CI sends a hosted hub (youi.record/v1, redacted, no code): not derived from git like the
|
|
128
|
+
// rest, so a rebuild (reset) keeps it; each repository's last 100 are kept.
|
|
129
|
+
{ id: 4, name: "ci-records", statements: ({ json: json2 }) => [
|
|
130
|
+
`CREATE TABLE IF NOT EXISTS ci_records (id TEXT PRIMARY KEY, repo TEXT NOT NULL, commit_sha TEXT, ref TEXT, generated_at TEXT NOT NULL, received_at TEXT NOT NULL, record ${json2} NOT NULL)`,
|
|
131
|
+
`CREATE INDEX IF NOT EXISTS ci_records_repo ON ci_records (repo, generated_at)`
|
|
132
|
+
] }
|
|
133
|
+
];
|
|
134
|
+
var SCHEMA_VERSION = Math.max(...MIGRATIONS.map((m) => m.id));
|
|
135
|
+
|
|
136
|
+
// ../store/src/store.ts
|
|
137
|
+
var TABLES = ["meta", "discover_items", "discover_roles", "signals", "licences", "scans", "families", "looks", "nodes", "edges"];
|
|
138
|
+
var MAX_PARAMS = 3e4;
|
|
139
|
+
var MAX_DEPTH = 6;
|
|
140
|
+
var RECORDS_KEPT = 100;
|
|
141
|
+
var SOURCE_CLASSES = ["official", "community", "npm", "other"];
|
|
142
|
+
var json = (value) => JSON.stringify(value);
|
|
143
|
+
var parse = (value) => typeof value === "string" ? JSON.parse(value) : value;
|
|
144
|
+
var int = (value) => Number(value ?? 0);
|
|
145
|
+
var marks = (n) => Array.from({ length: n }, () => "?").join(", ");
|
|
146
|
+
var batches = (items) => Array.from({ length: Math.ceil(items.length / MAX_PARAMS) }, (_, i) => items.slice(i * MAX_PARAMS, (i + 1) * MAX_PARAMS));
|
|
147
|
+
var SqlStore = class {
|
|
148
|
+
db;
|
|
149
|
+
lock = new Lock();
|
|
150
|
+
constructor(adapter) {
|
|
151
|
+
this.db = adapter;
|
|
152
|
+
}
|
|
153
|
+
get engine() {
|
|
154
|
+
return this.db.engine;
|
|
155
|
+
}
|
|
156
|
+
get location() {
|
|
157
|
+
return this.db.location;
|
|
158
|
+
}
|
|
159
|
+
async checkSchema() {
|
|
160
|
+
try {
|
|
161
|
+
const rows = await this.db.all(`SELECT MAX(id) AS id FROM migrations`);
|
|
162
|
+
if (int(rows[0]?.id) !== SCHEMA_VERSION) throw new Error("schema mismatch");
|
|
163
|
+
} catch {
|
|
164
|
+
throw new Error("Index schema is outdated: run youiyoux index rebuild");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Apply pending migrations, each in its own transaction. Safe to run on every open. */
|
|
168
|
+
async migrate() {
|
|
169
|
+
return this.lock.run(async () => {
|
|
170
|
+
await this.db.exec(`CREATE TABLE IF NOT EXISTS migrations (id INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL)`);
|
|
171
|
+
const done = new Set((await this.db.all(`SELECT id FROM migrations`)).map((r) => int(r.id)));
|
|
172
|
+
const applied = [];
|
|
173
|
+
for (const migration of MIGRATIONS) {
|
|
174
|
+
if (done.has(migration.id)) continue;
|
|
175
|
+
await this.db.exec(this.db.begin);
|
|
176
|
+
try {
|
|
177
|
+
for (const statement of migration.statements(this.db)) await this.db.exec(statement);
|
|
178
|
+
await this.db.run(`INSERT INTO migrations (id, name, applied_at) VALUES (?, ?, ?)`, [migration.id, migration.name, (/* @__PURE__ */ new Date()).toISOString()]);
|
|
179
|
+
await this.db.exec("COMMIT");
|
|
180
|
+
} catch (error) {
|
|
181
|
+
await this.db.exec("ROLLBACK");
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
applied.push(migration.id);
|
|
185
|
+
}
|
|
186
|
+
for (const statement of this.db.optional ?? []) await this.db.exec(statement).catch(() => {
|
|
187
|
+
});
|
|
188
|
+
return applied;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
write(fn) {
|
|
192
|
+
return this.lock.run(async () => {
|
|
193
|
+
await this.db.exec(this.db.begin);
|
|
194
|
+
try {
|
|
195
|
+
const result = await fn();
|
|
196
|
+
await this.db.exec("COMMIT");
|
|
197
|
+
return result;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
await this.db.exec("ROLLBACK").catch(() => {
|
|
200
|
+
});
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/** Rows in batches of one multi-row INSERT each. `columns` are SQL names (quote reserved ones). */
|
|
206
|
+
async insert(table, columns, rows, conflict) {
|
|
207
|
+
const size = Math.max(1, Math.floor(MAX_PARAMS / columns.length));
|
|
208
|
+
for (let i = 0; i < rows.length; i += size) {
|
|
209
|
+
const batch = rows.slice(i, i + size);
|
|
210
|
+
const values = batch.map(() => `(${marks(columns.length)})`).join(", ");
|
|
211
|
+
await this.db.run(`INSERT INTO ${table} (${columns.join(", ")}) VALUES ${values} ${conflict}`, batch.flat());
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async close() {
|
|
215
|
+
await this.lock.run(() => this.db.close());
|
|
216
|
+
}
|
|
217
|
+
async reset() {
|
|
218
|
+
await this.write(async () => {
|
|
219
|
+
for (const table of TABLES) await this.db.run(`DELETE FROM ${table}`);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
async stats() {
|
|
223
|
+
const tables = {};
|
|
224
|
+
for (const table of TABLES) tables[table] = int((await this.db.all(`SELECT CAST(COUNT(*) AS INTEGER) AS n FROM ${table}`))[0]?.n);
|
|
225
|
+
const schema = int((await this.db.all(`SELECT MAX(id) AS id FROM migrations`))[0]?.id);
|
|
226
|
+
return { engine: this.engine, location: this.location, schema: schema || SCHEMA_VERSION, generation: await this.meta("generation"), tables };
|
|
227
|
+
}
|
|
228
|
+
async meta(key) {
|
|
229
|
+
const row = (await this.db.all(`SELECT value FROM meta WHERE key = ?`, [key]))[0];
|
|
230
|
+
return row ? row.value : null;
|
|
231
|
+
}
|
|
232
|
+
async setMeta(key, value) {
|
|
233
|
+
await this.write(() => this.db.run(`INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value`, [key, value]));
|
|
234
|
+
}
|
|
235
|
+
/* ------------------------------------------------------------------ Discover */
|
|
236
|
+
async putItems(items) {
|
|
237
|
+
const columns = ["id", "scope", "ref", "name", "title", "summary", "kind", "source_kind", "source_class", "registry", "licence", "style", "downloads", "stars", "archived", "search_text", "entry", "built_at"];
|
|
238
|
+
const update = columns.slice(1).map((c) => `${c} = excluded.${c}`).join(", ");
|
|
239
|
+
await this.insert("discover_items", columns, items.map((i) => [i.id, i.scope, i.ref, i.name, i.title, i.summary, i.kind, i.sourceKind, i.sourceClass, i.registry, i.licence, i.style, i.downloads, i.stars, i.archived ? 1 : 0, i.searchText, json(i.entry), i.builtAt]), `ON CONFLICT (id) DO UPDATE SET ${update}`);
|
|
240
|
+
const ids = items.map((i) => i.id);
|
|
241
|
+
for (const batch of batches(ids)) await this.db.run(`DELETE FROM discover_roles WHERE item_id IN (${marks(batch.length)})`, batch);
|
|
242
|
+
await this.insert("discover_roles", ["item_id", "role"], items.flatMap((i) => [...new Set(i.roles)].map((role) => [i.id, role])), "ON CONFLICT (item_id, role) DO NOTHING");
|
|
243
|
+
}
|
|
244
|
+
async replaceDiscoverItems(scope, items) {
|
|
245
|
+
await this.write(async () => {
|
|
246
|
+
await this.db.run(`DELETE FROM discover_roles WHERE item_id IN (SELECT id FROM discover_items WHERE scope = ?)`, [scope]);
|
|
247
|
+
await this.db.run(`DELETE FROM discover_items WHERE scope = ?`, [scope]);
|
|
248
|
+
await this.putItems(items.map((i) => ({ ...i, scope })));
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
async upsertDiscoverItems(items) {
|
|
252
|
+
await this.write(() => this.putItems(items));
|
|
253
|
+
}
|
|
254
|
+
/** WHERE conditions for a filter, on discover_items aliased `d`. */
|
|
255
|
+
filterSql(filter) {
|
|
256
|
+
const parts = [];
|
|
257
|
+
const params = [];
|
|
258
|
+
const any = (column, values, lower = false) => {
|
|
259
|
+
if (!values?.length) return;
|
|
260
|
+
parts.push(`${lower ? `LOWER(${column})` : column} IN (${marks(values.length)})`);
|
|
261
|
+
params.push(...values.map((v) => lower ? v.toLowerCase() : v));
|
|
262
|
+
};
|
|
263
|
+
any("d.scope", filter.scopes.length ? filter.scopes : ["chosen"]);
|
|
264
|
+
any("d.source_kind", filter.sourceKinds);
|
|
265
|
+
any("d.source_class", filter.sourceClasses);
|
|
266
|
+
any("d.registry", filter.registries, true);
|
|
267
|
+
any("d.licence", filter.licences, true);
|
|
268
|
+
any("d.kind", filter.kinds);
|
|
269
|
+
any("d.style", filter.styles);
|
|
270
|
+
if (filter.roles?.length) {
|
|
271
|
+
parts.push(`d.id IN (SELECT item_id FROM discover_roles WHERE role IN (${marks(filter.roles.length)}))`);
|
|
272
|
+
params.push(...filter.roles);
|
|
273
|
+
}
|
|
274
|
+
return { sql: parts.join(" AND "), params };
|
|
275
|
+
}
|
|
276
|
+
async matchDiscover(query) {
|
|
277
|
+
if (query.terms.length === 0) return [];
|
|
278
|
+
const filtered = [query.roles, query.sourceKinds, query.sourceClasses, query.registries, query.licences, query.kinds, query.styles].some((v) => v?.length);
|
|
279
|
+
if (!filtered && this.db.top) {
|
|
280
|
+
const rows = await this.db.top(query.terms, query.scopes.length ? query.scopes : ["chosen"], SOURCE_CLASSES, pageInteger(query.perClass, 400, 1e4));
|
|
281
|
+
return rows.map((r) => ({ id: r.id, sourceClass: r.source_class, entry: parse(r.entry) }));
|
|
282
|
+
}
|
|
283
|
+
const match = this.db.match(query.terms);
|
|
284
|
+
const filter = this.filterSql(query);
|
|
285
|
+
const ranked = await this.db.all(
|
|
286
|
+
`SELECT id, source_class, r FROM (
|
|
287
|
+
SELECT id, source_class, r, ROW_NUMBER() OVER (PARTITION BY source_class ORDER BY r, id) AS n
|
|
288
|
+
FROM (SELECT d.id, d.source_class, ${match.rank} AS r FROM ${match.from} WHERE ${match.where} AND ${filter.sql}) matched
|
|
289
|
+
) best WHERE n <= CAST(? AS INTEGER) ORDER BY r, id`,
|
|
290
|
+
[...match.rankParams, ...match.whereParams, ...filter.params, pageInteger(query.perClass, 400, 1e4)]
|
|
291
|
+
);
|
|
292
|
+
const entries = /* @__PURE__ */ new Map();
|
|
293
|
+
const ids = ranked.map((r) => r.id);
|
|
294
|
+
for (const batch of batches(ids)) {
|
|
295
|
+
for (const row of await this.db.all(`SELECT id, entry FROM discover_items WHERE id IN (${marks(batch.length)})`, batch)) entries.set(row.id, row.entry);
|
|
296
|
+
}
|
|
297
|
+
return ranked.map((r) => ({ id: r.id, sourceClass: r.source_class, entry: parse(entries.get(r.id)) }));
|
|
298
|
+
}
|
|
299
|
+
async listDiscover(filter, page = {}) {
|
|
300
|
+
const where = this.filterSql(filter);
|
|
301
|
+
const limit = pageInteger(page.limit, 20);
|
|
302
|
+
const offset = pageInteger(page.offset, 0, 2147483647);
|
|
303
|
+
const rows = await this.db.all(
|
|
304
|
+
`SELECT d.id, d.source_class, d.entry FROM discover_items d WHERE ${where.sql} ORDER BY CASE d.source_class WHEN 'official' THEN 0 ELSE 1 END, COALESCE(d.downloads, -1) DESC, d.id LIMIT ? OFFSET ?`,
|
|
305
|
+
[...where.params, limit, offset]
|
|
306
|
+
);
|
|
307
|
+
return { rows: rows.map((r) => ({ id: r.id, sourceClass: r.source_class, entry: parse(r.entry) })), total: await this.countDiscover(filter) };
|
|
308
|
+
}
|
|
309
|
+
async countDiscover(filter) {
|
|
310
|
+
const where = this.filterSql(filter);
|
|
311
|
+
return int((await this.db.all(`SELECT CAST(COUNT(*) AS INTEGER) AS n FROM discover_items d WHERE ${where.sql}`, where.params))[0]?.n);
|
|
312
|
+
}
|
|
313
|
+
async discoverFacets(filter) {
|
|
314
|
+
const where = this.filterSql(filter);
|
|
315
|
+
const group = async (column) => {
|
|
316
|
+
const rows = await this.db.all(`SELECT ${column} AS k, CAST(COUNT(*) AS INTEGER) AS n FROM discover_items d WHERE ${where.sql} GROUP BY ${column} ORDER BY n DESC, k`, where.params);
|
|
317
|
+
return Object.fromEntries(rows.map((r) => [r.k ?? "unknown", int(r.n)]));
|
|
318
|
+
};
|
|
319
|
+
const roles = await this.db.all(`SELECT r.role AS k, CAST(COUNT(*) AS INTEGER) AS n FROM discover_roles r JOIN discover_items d ON d.id = r.item_id WHERE ${where.sql} GROUP BY r.role ORDER BY n DESC, k`, where.params);
|
|
320
|
+
return { role: Object.fromEntries(roles.map((r) => [r.k, int(r.n)])), source: await group("COALESCE(d.registry, d.source_kind)"), licence: await group("d.licence"), style: await group("d.style") };
|
|
321
|
+
}
|
|
322
|
+
async getDiscoverItem(idOrRef, scopes) {
|
|
323
|
+
const scoped = scopes.length ? scopes : ["chosen"];
|
|
324
|
+
for (const column of ["id", "ref"]) {
|
|
325
|
+
const row = (await this.db.all(`SELECT id, source_class, entry FROM discover_items WHERE ${column} = ? AND scope IN (${marks(scoped.length)}) ORDER BY CASE scope WHEN 'chosen' THEN 0 ELSE 1 END, id LIMIT 1`, [idOrRef, ...scoped]))[0];
|
|
326
|
+
if (row) return { id: row.id, sourceClass: row.source_class, entry: parse(row.entry) };
|
|
327
|
+
}
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
async discoverRoles(itemIds) {
|
|
331
|
+
const out = {};
|
|
332
|
+
for (const batch of batches(itemIds)) {
|
|
333
|
+
for (const row of await this.db.all(`SELECT item_id, role FROM discover_roles WHERE item_id IN (${marks(batch.length)}) ORDER BY item_id, role`, batch)) (out[row.item_id] ??= []).push(row.role);
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
/* ------------------------------------------------------- signals and licences */
|
|
338
|
+
async replaceSignals(signals) {
|
|
339
|
+
await this.write(async () => {
|
|
340
|
+
await this.db.run(`DELETE FROM signals`);
|
|
341
|
+
await this.insert("signals", ["key", "kind", "data", "fetched_at"], signals.map((s) => [s.key, s.kind, json(s.data), s.fetchedAt]), "ON CONFLICT (key) DO UPDATE SET kind = excluded.kind, data = excluded.data, fetched_at = excluded.fetched_at");
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
async signal(key) {
|
|
345
|
+
const row = (await this.db.all(`SELECT key, kind, data, fetched_at FROM signals WHERE key = ?`, [key]))[0];
|
|
346
|
+
return row ? { key: row.key, kind: row.kind, data: parse(row.data), fetchedAt: row.fetched_at } : null;
|
|
347
|
+
}
|
|
348
|
+
async replaceLicences(licences) {
|
|
349
|
+
await this.write(async () => {
|
|
350
|
+
await this.db.run(`DELETE FROM licences`);
|
|
351
|
+
await this.insert("licences", ["item_id", "spdx", "evidence_url", "verified_at"], licences.map((l) => [l.itemId, l.spdx, l.evidenceUrl, l.verifiedAt]), "ON CONFLICT (item_id) DO UPDATE SET spdx = excluded.spdx, evidence_url = excluded.evidence_url, verified_at = excluded.verified_at");
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
async licence(itemId) {
|
|
355
|
+
const row = (await this.db.all(`SELECT item_id, spdx, evidence_url, verified_at FROM licences WHERE item_id = ?`, [itemId]))[0];
|
|
356
|
+
return row ? { itemId: row.item_id, spdx: row.spdx, evidenceUrl: row.evidence_url, verifiedAt: row.verified_at } : null;
|
|
357
|
+
}
|
|
358
|
+
/* ---------------------------------------------------------------- CI records */
|
|
359
|
+
async putRecord(record, receivedAt) {
|
|
360
|
+
await this.write(async () => {
|
|
361
|
+
const id = `${record.repo}@${record.commit ?? record.generatedAt}`;
|
|
362
|
+
await this.insert("ci_records", ["id", "repo", "commit_sha", "ref", "generated_at", "received_at", "record"], [[id, record.repo, record.commit, record.ref, record.generatedAt, receivedAt, json(record)]], "ON CONFLICT (id) DO UPDATE SET ref = excluded.ref, generated_at = excluded.generated_at, received_at = excluded.received_at, record = excluded.record");
|
|
363
|
+
await this.db.run(`DELETE FROM ci_records WHERE repo = ? AND id NOT IN (SELECT id FROM ci_records WHERE repo = ? ORDER BY generated_at DESC, received_at DESC LIMIT ${RECORDS_KEPT})`, [record.repo, record.repo]);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
async latestRecords() {
|
|
367
|
+
const rows = await this.db.all(`SELECT record, received_at FROM (SELECT record, received_at, repo, ROW_NUMBER() OVER (PARTITION BY repo ORDER BY generated_at DESC, received_at DESC) AS n FROM ci_records) ranked WHERE n = 1 ORDER BY repo`);
|
|
368
|
+
return rows.map((r) => ({ record: parse(r.record), receivedAt: String(r.received_at) }));
|
|
369
|
+
}
|
|
370
|
+
/* ----------------------------------------------------------------- inventory */
|
|
371
|
+
async recordInventory(run) {
|
|
372
|
+
await this.write(async () => {
|
|
373
|
+
await this.insert(
|
|
374
|
+
"scans",
|
|
375
|
+
["id", "run", "source", "project", "root", "commit_sha", "scanned_at", "status", "files", "looks", "families", "near_duplicates", "roles"],
|
|
376
|
+
run.projects.map((p) => [`${run.run}:${p.project}`, run.run, run.source, p.project, p.root, p.commit, run.scannedAt, p.status, p.files, p.looks, p.families, p.nearDuplicates, json(p.roles)]),
|
|
377
|
+
"ON CONFLICT (id) DO UPDATE SET commit_sha = excluded.commit_sha, scanned_at = excluded.scanned_at, status = excluded.status, files = excluded.files, looks = excluded.looks, families = excluded.families, near_duplicates = excluded.near_duplicates, roles = excluded.roles"
|
|
378
|
+
);
|
|
379
|
+
await this.db.run(`DELETE FROM families WHERE run = ?`, [run.run]);
|
|
380
|
+
await this.insert("families", ["run", "id", "role", "looks", "near_duplicate", "projects", "kits", "shape"], run.families.map((f) => [run.run, f.id, f.role, f.looks, f.nearDuplicate ? 1 : 0, json(f.projects), json(f.kits), json(f.shape)]), "ON CONFLICT (run, id) DO NOTHING");
|
|
381
|
+
const newest = (await this.db.all(`SELECT MAX(scanned_at) AS scanned_at FROM scans WHERE source = ?`, [run.source]))[0]?.scanned_at;
|
|
382
|
+
if (newest && newest > run.scannedAt) return;
|
|
383
|
+
await this.db.run(`DELETE FROM looks WHERE run IN (SELECT DISTINCT run FROM scans WHERE source = ?)`, [run.source]);
|
|
384
|
+
await this.insert("looks", ["run", "id", "project", "role", "file", "line", "col", "tag", "kit", "family", "routes", "shape"], run.looks.map((l) => [run.run, l.id, l.project, l.role, l.file, l.line, l.column, l.tag, l.kit, l.family, json(l.routes), json(l.shape)]), "ON CONFLICT (run, id) DO NOTHING");
|
|
385
|
+
await this.putGraph(run.source, run.graph);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
async history(filter = {}) {
|
|
389
|
+
const where = filter.project ? `WHERE project = ?` : "";
|
|
390
|
+
const rows = await this.db.all(`SELECT * FROM scans ${where} ORDER BY scanned_at DESC, project LIMIT ? OFFSET ?`, [...filter.project ? [filter.project] : [], pageInteger(filter.limit, 20), pageInteger(filter.offset, 0, 2147483647)]);
|
|
391
|
+
return rows.map((r) => ({ id: String(r.id), run: String(r.run), source: String(r.source), project: String(r.project), root: String(r.root), commit: r.commit_sha ?? null, scannedAt: String(r.scanned_at), status: String(r.status), files: int(r.files), looks: int(r.looks), families: int(r.families), nearDuplicates: int(r.near_duplicates), roles: parse(r.roles) }));
|
|
392
|
+
}
|
|
393
|
+
async latestRun(source) {
|
|
394
|
+
const row = (await this.db.all(`SELECT run, source, scanned_at FROM scans ${source ? "WHERE source = ?" : ""} ORDER BY scanned_at DESC, run DESC LIMIT 1`, source ? [source] : []))[0];
|
|
395
|
+
return row ? { run: row.run, source: row.source, scannedAt: row.scanned_at } : null;
|
|
396
|
+
}
|
|
397
|
+
async families(filter = {}) {
|
|
398
|
+
const run = filter.run ?? (await this.latestRun())?.run ?? null;
|
|
399
|
+
if (!run) return { run: null, total: 0, rows: [] };
|
|
400
|
+
const where = `run = ?${filter.role ? " AND role = ?" : ""}`;
|
|
401
|
+
const params = [run, ...filter.role ? [filter.role] : []];
|
|
402
|
+
const total = int((await this.db.all(`SELECT CAST(COUNT(*) AS INTEGER) AS n FROM families WHERE ${where}`, params))[0]?.n);
|
|
403
|
+
const rows = await this.db.all(`SELECT * FROM families WHERE ${where} ORDER BY role, looks DESC, id LIMIT ? OFFSET ?`, [...params, pageInteger(filter.limit, 50), pageInteger(filter.offset, 0, 2147483647)]);
|
|
404
|
+
return { run, total, rows: rows.map((r) => ({ id: String(r.id), role: String(r.role), looks: int(r.looks), nearDuplicate: int(r.near_duplicate) === 1, projects: parse(r.projects), kits: parse(r.kits), shape: parse(r.shape) })) };
|
|
405
|
+
}
|
|
406
|
+
async looks(filter) {
|
|
407
|
+
const run = filter.run ?? (await this.latestRun())?.run ?? null;
|
|
408
|
+
if (!run) return { run: null, total: 0, rows: [] };
|
|
409
|
+
const where = `run = ?${filter.family ? " AND family = ?" : ""}${filter.role ? " AND role = ?" : ""}`;
|
|
410
|
+
const params = [run, ...filter.family ? [filter.family] : [], ...filter.role ? [filter.role] : []];
|
|
411
|
+
const total = int((await this.db.all(`SELECT CAST(COUNT(*) AS INTEGER) AS n FROM looks WHERE ${where}`, params))[0]?.n);
|
|
412
|
+
const rows = await this.db.all(`SELECT * FROM looks WHERE ${where} ORDER BY project, file, line, col, id LIMIT ? OFFSET ?`, [...params, pageInteger(filter.limit, 50), pageInteger(filter.offset, 0, 2147483647)]);
|
|
413
|
+
return { run, total, rows: rows.map((r) => ({ id: String(r.id), project: String(r.project), role: String(r.role), file: String(r.file), line: int(r.line), column: int(r.col), tag: String(r.tag), kit: r.kit ?? null, family: r.family ?? null, routes: parse(r.routes), shape: parse(r.shape) })) };
|
|
414
|
+
}
|
|
415
|
+
/* --------------------------------------------------------------------- graph */
|
|
416
|
+
async putGraph(source, graph) {
|
|
417
|
+
await this.db.run(`DELETE FROM edges WHERE source = ?`, [source]);
|
|
418
|
+
await this.db.run(`DELETE FROM nodes WHERE source = ? AND id NOT IN (SELECT "from" FROM edges) AND id NOT IN (SELECT "to" FROM edges)`, [source]);
|
|
419
|
+
const nodes = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
420
|
+
await this.insert("nodes", ["id", "kind", "ref", "source", "project"], [...nodes.values()].map((n) => [n.id, n.kind, n.ref, source, n.project ?? null]), "ON CONFLICT (id) DO UPDATE SET kind = excluded.kind, ref = excluded.ref, project = excluded.project");
|
|
421
|
+
const edges = new Map(graph.edges.map((e) => [`${e.from}\0${e.to}\0${e.kind}`, e]));
|
|
422
|
+
await this.insert("edges", [`"from"`, `"to"`, "kind", "source"], [...edges.values()].map((e) => [e.from, e.to, e.kind, source]), `ON CONFLICT ("from", "to", kind) DO UPDATE SET source = excluded.source`);
|
|
423
|
+
}
|
|
424
|
+
async replaceGraph(source, graph) {
|
|
425
|
+
await this.write(() => this.putGraph(source, graph));
|
|
426
|
+
}
|
|
427
|
+
async reach(start, kinds, options = {}) {
|
|
428
|
+
if (start.length === 0 || kinds.length === 0) return [];
|
|
429
|
+
const [near, far] = options.reverse ? [`"to"`, `"from"`] : [`"from"`, `"to"`];
|
|
430
|
+
const depth = Math.max(0, Math.min(MAX_DEPTH, options.maxDepth ?? 3));
|
|
431
|
+
const rows = await this.db.all(
|
|
432
|
+
`WITH RECURSIVE reach(id, depth) AS (
|
|
433
|
+
SELECT id, 0 FROM nodes WHERE id IN (${marks(start.length)})
|
|
434
|
+
UNION
|
|
435
|
+
SELECT e.${far}, r.depth + 1 FROM edges e JOIN reach r ON e.${near} = r.id
|
|
436
|
+
WHERE e.kind IN (${marks(kinds.length)}) AND r.depth < CAST(? AS INTEGER)
|
|
437
|
+
)
|
|
438
|
+
SELECT n.id, n.kind, n.ref, MIN(r.depth) AS depth FROM reach r JOIN nodes n ON n.id = r.id
|
|
439
|
+
GROUP BY n.id, n.kind, n.ref ORDER BY MIN(r.depth), n.kind, n.ref`,
|
|
440
|
+
[...start, ...kinds, depth]
|
|
441
|
+
);
|
|
442
|
+
return rows.map((r) => ({ id: r.id, kind: r.kind, ref: r.ref, depth: int(r.depth) }));
|
|
443
|
+
}
|
|
444
|
+
async findNodes(kind, ref) {
|
|
445
|
+
return this.db.all(`SELECT id, kind, ref, source, project FROM nodes WHERE kind = ? AND (ref = ? OR id = ?) ORDER BY id`, [kind, ref, ref]);
|
|
446
|
+
}
|
|
447
|
+
async nodesOfKind(kind, source) {
|
|
448
|
+
return this.db.all(`SELECT id, kind, ref, source, project FROM nodes WHERE kind = ?${source ? " AND source = ?" : ""} ORDER BY id`, source ? [kind, source] : [kind]);
|
|
449
|
+
}
|
|
450
|
+
async resolveFamily(ref, project) {
|
|
451
|
+
const found = (await this.findNodes("family", ref)).filter((n) => project === void 0 || n.project === project);
|
|
452
|
+
return found.length > 1 ? { choices: found } : found[0] ?? null;
|
|
453
|
+
}
|
|
454
|
+
async resolveRule(ref, project) {
|
|
455
|
+
const escaped = ref.toLowerCase().replace(/[!%_]/g, "!$&");
|
|
456
|
+
const found = await this.db.all(
|
|
457
|
+
`SELECT id, kind, ref, source, project FROM nodes WHERE kind = 'rule' AND (LOWER(ref) = ? OR LOWER(id) = ? OR LOWER(ref) LIKE ? ESCAPE '!')${project === void 0 ? "" : " AND project = ?"} ORDER BY id`,
|
|
458
|
+
[ref.toLowerCase(), ref.toLowerCase(), `%/rule/${escaped}`, ...project === void 0 ? [] : [project]]
|
|
459
|
+
);
|
|
460
|
+
return found.length > 1 ? { choices: found } : found[0] ?? null;
|
|
461
|
+
}
|
|
462
|
+
async familyLooks(source, ids, project) {
|
|
463
|
+
const run = (await this.latestRun(source))?.run;
|
|
464
|
+
if (!run || ids.length === 0) return /* @__PURE__ */ new Map();
|
|
465
|
+
const rows = await this.db.all(`SELECT family AS id, COUNT(*) AS looks, MIN(role) AS role FROM looks WHERE run = ? AND family IN (${marks(ids.length)})${project == null ? "" : " AND project = ?"} GROUP BY family`, [run, ...ids, ...project == null ? [] : [project]]);
|
|
466
|
+
return new Map(rows.map((r) => [r.id, { looks: int(r.looks), role: r.role }]));
|
|
467
|
+
}
|
|
468
|
+
async whereUsed(family, options = {}) {
|
|
469
|
+
const node = await this.resolveFamily(family, options.project);
|
|
470
|
+
if (!node || "choices" in node) return node;
|
|
471
|
+
const used = await this.reach([node.id], ["used-in"], { maxDepth: 2 });
|
|
472
|
+
const near = await this.reach([node.id], ["near-duplicate-of"], { maxDepth: 1 });
|
|
473
|
+
const info = (await this.familyLooks(node.source, [node.ref], node.project)).get(node.ref);
|
|
474
|
+
return {
|
|
475
|
+
family: node.ref,
|
|
476
|
+
role: info?.role ?? null,
|
|
477
|
+
looks: info?.looks ?? 0,
|
|
478
|
+
files: used.filter((n) => n.kind === "file").map((n) => n.ref),
|
|
479
|
+
routes: used.filter((n) => n.kind === "route").map((n) => n.ref),
|
|
480
|
+
nearDuplicates: near.filter((n) => n.depth > 0 && n.kind === "family").map((n) => n.ref),
|
|
481
|
+
source: node.source
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
async swapImpact(candidate, family, options = {}) {
|
|
485
|
+
const node = await this.resolveFamily(family, options.project);
|
|
486
|
+
if (!node || "choices" in node) return node;
|
|
487
|
+
const cand = (await this.findNodes("candidate", candidate))[0];
|
|
488
|
+
const replaces = cand ? await this.reach([cand.id], ["could-replace"], { maxDepth: 1 }) : [];
|
|
489
|
+
const twins = (await this.reach([node.id], ["near-duplicate-of"], { maxDepth: 4 })).filter((n) => n.kind === "family");
|
|
490
|
+
const used = await this.reach(twins.map((t) => t.id), ["used-in"], { maxDepth: 2 });
|
|
491
|
+
const looks = await this.familyLooks(node.source, twins.map((t) => t.ref), node.project);
|
|
492
|
+
const others = await this.reach([node.id], ["could-replace"], { maxDepth: 1, reverse: true });
|
|
493
|
+
return {
|
|
494
|
+
candidate,
|
|
495
|
+
family: node.ref,
|
|
496
|
+
listed: replaces.some((n) => n.id === node.id),
|
|
497
|
+
families: twins.map((t) => ({ id: t.ref, looks: looks.get(t.ref)?.looks ?? 0, depth: t.depth })),
|
|
498
|
+
looks: twins.reduce((n, t) => n + (looks.get(t.ref)?.looks ?? 0), 0),
|
|
499
|
+
files: used.filter((n) => n.kind === "file").map((n) => n.ref),
|
|
500
|
+
routes: used.filter((n) => n.kind === "route").map((n) => n.ref),
|
|
501
|
+
otherCandidates: others.filter((n) => n.kind === "candidate" && n.ref !== candidate).map((n) => n.ref)
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
async ruleCovers(rule, options = {}) {
|
|
505
|
+
const node = await this.resolveRule(rule, options.project);
|
|
506
|
+
if (!node || "choices" in node) return node;
|
|
507
|
+
const reached = await this.reach([node.id], ["governs", "used-in"], { maxDepth: 3 });
|
|
508
|
+
return {
|
|
509
|
+
rule: node.ref,
|
|
510
|
+
components: reached.filter((n) => n.kind === "component").map((n) => n.ref),
|
|
511
|
+
files: reached.filter((n) => n.kind === "file").map((n) => n.ref),
|
|
512
|
+
routes: reached.filter((n) => n.kind === "route").map((n) => n.ref)
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// ../store/src/sqlite.ts
|
|
518
|
+
import { mkdirSync, statSync } from "node:fs";
|
|
519
|
+
import { createRequire } from "node:module";
|
|
520
|
+
import { dirname } from "node:path";
|
|
521
|
+
var STATEMENT_CACHE = 200;
|
|
522
|
+
var sqlite = null;
|
|
523
|
+
function openDatabase(...args) {
|
|
524
|
+
sqlite ??= createRequire(import.meta.url)("node:sqlite");
|
|
525
|
+
return new sqlite.DatabaseSync(...args);
|
|
526
|
+
}
|
|
527
|
+
var words = (terms) => searchTerms(terms).map((t) => `"${t.replace(/"/g, "")}"`).join(" OR ");
|
|
528
|
+
function sqliteAdapter(path, options = {}) {
|
|
529
|
+
if (!options.readOnly && path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
|
|
530
|
+
const db = openDatabase(path, { timeout: 5e3, readOnly: options.readOnly ?? false });
|
|
531
|
+
if (!options.readOnly) db.exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;");
|
|
532
|
+
const statements = /* @__PURE__ */ new Map();
|
|
533
|
+
const prepare = (sql) => {
|
|
534
|
+
let statement = statements.get(sql);
|
|
535
|
+
if (!statement) {
|
|
536
|
+
if (statements.size >= STATEMENT_CACHE) statements.delete(statements.keys().next().value);
|
|
537
|
+
statement = db.prepare(sql);
|
|
538
|
+
statements.set(sql, statement);
|
|
539
|
+
}
|
|
540
|
+
return statement;
|
|
541
|
+
};
|
|
542
|
+
const values = (params) => params;
|
|
543
|
+
return {
|
|
544
|
+
engine: "sqlite",
|
|
545
|
+
location: path,
|
|
546
|
+
json: "TEXT",
|
|
547
|
+
begin: "BEGIN IMMEDIATE",
|
|
548
|
+
searchDdl: [
|
|
549
|
+
// The listing's words, and its scope and source class as tag words (`schosen ccommunity`),
|
|
550
|
+
// so an unfiltered search ranks inside the index alone. Kept in step by rowid.
|
|
551
|
+
`CREATE VIRTUAL TABLE IF NOT EXISTS discover_fts USING fts5(search_text, tags, tokenize='porter unicode61')`,
|
|
552
|
+
`CREATE TRIGGER IF NOT EXISTS discover_items_ai AFTER INSERT ON discover_items BEGIN
|
|
553
|
+
INSERT INTO discover_fts (rowid, search_text, tags) VALUES (new.rowid, new.search_text, 's' || new.scope || ' c' || new.source_class);
|
|
554
|
+
END`,
|
|
555
|
+
`CREATE TRIGGER IF NOT EXISTS discover_items_ad AFTER DELETE ON discover_items BEGIN
|
|
556
|
+
DELETE FROM discover_fts WHERE rowid = old.rowid;
|
|
557
|
+
END`,
|
|
558
|
+
`CREATE TRIGGER IF NOT EXISTS discover_items_au AFTER UPDATE ON discover_items BEGIN
|
|
559
|
+
DELETE FROM discover_fts WHERE rowid = old.rowid;
|
|
560
|
+
INSERT INTO discover_fts (rowid, search_text, tags) VALUES (new.rowid, new.search_text, 's' || new.scope || ' c' || new.source_class);
|
|
561
|
+
END`
|
|
562
|
+
],
|
|
563
|
+
match: (terms) => ({
|
|
564
|
+
from: `discover_fts JOIN discover_items d ON d.rowid = discover_fts.rowid`,
|
|
565
|
+
where: searchTerms(terms).length ? `discover_fts MATCH ?` : `0 = 1 AND ? = ?`,
|
|
566
|
+
whereParams: searchTerms(terms).length ? [`search_text : (${words(terms)})`] : [0, 1],
|
|
567
|
+
rank: `bm25(discover_fts)`,
|
|
568
|
+
rankParams: []
|
|
569
|
+
}),
|
|
570
|
+
// One query per source class: the tag words narrow it, bm25 ranks only that class's matches, LIMIT keeps the best.
|
|
571
|
+
top: async (terms, scopes, classes, perClass) => {
|
|
572
|
+
if (!searchTerms(terms).length) return [];
|
|
573
|
+
const statement = prepare(`SELECT d.id, d.source_class, d.entry FROM (SELECT rowid AS rid, bm25(discover_fts) AS r FROM discover_fts WHERE discover_fts MATCH ? ORDER BY r LIMIT ?) m JOIN discover_items d ON d.rowid = m.rid ORDER BY m.r, d.id`);
|
|
574
|
+
const scope = words(scopes.map((s) => `s${s}`));
|
|
575
|
+
return classes.flatMap((c) => statement.all(`search_text : (${words(terms)}) AND tags : (${scope}) AND tags : ${words([`c${c}`])}`, perClass));
|
|
576
|
+
},
|
|
577
|
+
all: async (sql, params = []) => prepare(sql).all(...values(params)),
|
|
578
|
+
run: async (sql, params = []) => {
|
|
579
|
+
prepare(sql).run(...values(params));
|
|
580
|
+
},
|
|
581
|
+
exec: async (script) => db.exec(script),
|
|
582
|
+
close: async () => {
|
|
583
|
+
statements.clear();
|
|
584
|
+
if (db.isOpen) db.close();
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
async function openSqlite(path, options = {}) {
|
|
589
|
+
const connect = async () => {
|
|
590
|
+
const store = new SqlStore(sqliteAdapter(path, options));
|
|
591
|
+
try {
|
|
592
|
+
if (options.readOnly) await store.checkSchema();
|
|
593
|
+
else await store.migrate();
|
|
594
|
+
return store;
|
|
595
|
+
} catch (error) {
|
|
596
|
+
await store.close();
|
|
597
|
+
throw error;
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
const identity = () => {
|
|
601
|
+
const s = statSync(path);
|
|
602
|
+
return `${s.dev}:${s.ino}`;
|
|
603
|
+
};
|
|
604
|
+
let inode = null;
|
|
605
|
+
if (path !== ":memory:") {
|
|
606
|
+
try {
|
|
607
|
+
inode = identity();
|
|
608
|
+
} catch (error) {
|
|
609
|
+
if (error.code !== "ENOENT") throw error;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
let current = await connect();
|
|
613
|
+
if (path === ":memory:") return current;
|
|
614
|
+
inode ??= identity();
|
|
615
|
+
let generation = await current.meta("generation");
|
|
616
|
+
let checked = Date.now();
|
|
617
|
+
let closed = false;
|
|
618
|
+
const lock = new Lock();
|
|
619
|
+
return new Proxy(current, {
|
|
620
|
+
get(_target, key) {
|
|
621
|
+
const value = Reflect.get(current, key);
|
|
622
|
+
if (typeof value !== "function") return value;
|
|
623
|
+
return (...args) => lock.run(async () => {
|
|
624
|
+
if (key === "close") {
|
|
625
|
+
closed = true;
|
|
626
|
+
return current.close();
|
|
627
|
+
}
|
|
628
|
+
if (closed) throw new Error("Store is closed");
|
|
629
|
+
const next = identity();
|
|
630
|
+
const now = Date.now();
|
|
631
|
+
const nextGeneration = next === inode && now - checked >= 100 ? await current.meta("generation") : generation;
|
|
632
|
+
if (next !== inode || nextGeneration !== generation) {
|
|
633
|
+
await current.close();
|
|
634
|
+
const openingInode = identity();
|
|
635
|
+
current = await connect();
|
|
636
|
+
inode = openingInode;
|
|
637
|
+
generation = await current.meta("generation");
|
|
638
|
+
}
|
|
639
|
+
if (now - checked >= 100) checked = now;
|
|
640
|
+
return Reflect.apply(Reflect.get(current, key), current, args);
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
function checkpointSqlite(path) {
|
|
646
|
+
const db = openDatabase(path);
|
|
647
|
+
try {
|
|
648
|
+
const result = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
649
|
+
if (Number(result?.busy ?? 0)) throw new Error("Index checkpoint is busy; retry rebuild");
|
|
650
|
+
} finally {
|
|
651
|
+
db.close();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ../store/src/postgres.ts
|
|
656
|
+
var TRIGRAM_TERMS = 8;
|
|
657
|
+
function postgresAdapter(connection, location) {
|
|
658
|
+
const adapter = {
|
|
659
|
+
engine: "postgres",
|
|
660
|
+
location,
|
|
661
|
+
json: "JSONB",
|
|
662
|
+
begin: "BEGIN",
|
|
663
|
+
trigram: false,
|
|
664
|
+
searchDdl: [
|
|
665
|
+
// English stemming like FTS5's porter, but no stop-word list, so no asked-for word is ever dropped.
|
|
666
|
+
`DO $$ BEGIN
|
|
667
|
+
IF NOT EXISTS (SELECT 1 FROM pg_ts_dict WHERE dictname = 'youi_stem') THEN
|
|
668
|
+
CREATE TEXT SEARCH DICTIONARY youi_stem (TEMPLATE = snowball, Language = english);
|
|
669
|
+
END IF;
|
|
670
|
+
IF NOT EXISTS (SELECT 1 FROM pg_ts_config WHERE cfgname = 'youi') THEN
|
|
671
|
+
CREATE TEXT SEARCH CONFIGURATION youi (COPY = simple);
|
|
672
|
+
ALTER TEXT SEARCH CONFIGURATION youi ALTER MAPPING FOR asciiword, word, numword, asciihword, hword, numhword, hword_asciipart, hword_part, hword_numpart WITH youi_stem;
|
|
673
|
+
END IF;
|
|
674
|
+
END $$`,
|
|
675
|
+
`ALTER TABLE discover_items ADD COLUMN IF NOT EXISTS search_tsv tsvector GENERATED ALWAYS AS (to_tsvector('youi', search_text)) STORED`,
|
|
676
|
+
`CREATE INDEX IF NOT EXISTS discover_items_search ON discover_items USING GIN (search_tsv)`
|
|
677
|
+
],
|
|
678
|
+
optional: [`CREATE EXTENSION IF NOT EXISTS pg_trgm`, `CREATE INDEX IF NOT EXISTS discover_items_name_trgm ON discover_items USING GIN (name gin_trgm_ops)`],
|
|
679
|
+
match(terms) {
|
|
680
|
+
terms = searchTerms(terms);
|
|
681
|
+
const query = terms.join(" | ");
|
|
682
|
+
const prefixes = adapter.trigram ? terms.slice(0, TRIGRAM_TERMS).map((t) => `${t}%`) : [];
|
|
683
|
+
return {
|
|
684
|
+
from: `discover_items d`,
|
|
685
|
+
where: prefixes.length ? `(d.search_tsv @@ to_tsquery('youi', ?) OR ${prefixes.map(() => "d.name ILIKE ? ESCAPE '!'").join(" OR ")})` : `d.search_tsv @@ to_tsquery('youi', ?)`,
|
|
686
|
+
whereParams: [query, ...prefixes],
|
|
687
|
+
rank: `-ts_rank(d.search_tsv, to_tsquery('youi', ?))`,
|
|
688
|
+
rankParams: [query]
|
|
689
|
+
};
|
|
690
|
+
},
|
|
691
|
+
all: async (sql, params = []) => (await connection.query(numbered(sql), [...params])).rows,
|
|
692
|
+
run: async (sql, params = []) => {
|
|
693
|
+
await connection.query(numbered(sql), [...params]);
|
|
694
|
+
},
|
|
695
|
+
exec: (script) => connection.exec(script),
|
|
696
|
+
close: () => connection.close()
|
|
697
|
+
};
|
|
698
|
+
return adapter;
|
|
699
|
+
}
|
|
700
|
+
async function openWith(connection, location, options = {}) {
|
|
701
|
+
const adapter = postgresAdapter(connection, location);
|
|
702
|
+
const store = new SqlStore(adapter);
|
|
703
|
+
try {
|
|
704
|
+
if (options.readOnly) {
|
|
705
|
+
await connection.exec("SET default_transaction_read_only = on");
|
|
706
|
+
await store.checkSchema();
|
|
707
|
+
} else await store.migrate();
|
|
708
|
+
} catch (error) {
|
|
709
|
+
await store.close();
|
|
710
|
+
throw error;
|
|
711
|
+
}
|
|
712
|
+
adapter.trigram = (await adapter.all(`SELECT 1 AS ok FROM pg_extension WHERE extname = 'pg_trgm'`)).length > 0;
|
|
713
|
+
return store;
|
|
714
|
+
}
|
|
715
|
+
function redactUrl(url) {
|
|
716
|
+
try {
|
|
717
|
+
const parsed = new URL(url);
|
|
718
|
+
if (parsed.password) parsed.password = "***";
|
|
719
|
+
return parsed.toString();
|
|
720
|
+
} catch {
|
|
721
|
+
return "postgres://\u2026";
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
async function openPostgres(url, options = {}) {
|
|
725
|
+
const { default: pg } = await import("pg");
|
|
726
|
+
const client = new pg.Client({ connectionString: url });
|
|
727
|
+
await client.connect();
|
|
728
|
+
return openWith({ query: (sql, params) => client.query(sql, params), exec: async (script) => void await client.query(script), close: () => client.end() }, redactUrl(url), options);
|
|
729
|
+
}
|
|
730
|
+
async function openPglite(dataDir, options = {}) {
|
|
731
|
+
const { PGlite } = await import("@electric-sql/pglite");
|
|
732
|
+
const db = dataDir ? new PGlite(dataDir) : new PGlite();
|
|
733
|
+
await db.waitReady;
|
|
734
|
+
return openWith({ query: (sql, params) => db.query(sql, params), exec: async (script) => void await db.exec(script), close: () => db.close() }, dataDir ? `pglite:${dataDir}` : "pglite:memory", options);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// ../store/src/open.ts
|
|
738
|
+
import { homedir } from "node:os";
|
|
739
|
+
import { dirname as dirname2, join } from "node:path";
|
|
740
|
+
var INDEX_FILE = "index.db";
|
|
741
|
+
function defaultIndexPath(env = process.env) {
|
|
742
|
+
if (env.YOUI_INDEX) return env.YOUI_INDEX;
|
|
743
|
+
return join(env.XDG_CACHE_HOME || join(homedir(), ".cache"), "youiyoux", INDEX_FILE);
|
|
744
|
+
}
|
|
745
|
+
function indexHome(path, env = process.env) {
|
|
746
|
+
return path === ":memory:" ? dirname2(defaultIndexPath(env)) : dirname2(path);
|
|
747
|
+
}
|
|
748
|
+
function storeLocation(options = {}) {
|
|
749
|
+
const env = options.env ?? process.env;
|
|
750
|
+
return env.DATABASE_URL ? redactUrl(env.DATABASE_URL) : options.path ?? defaultIndexPath(env);
|
|
751
|
+
}
|
|
752
|
+
async function openStore(options = {}) {
|
|
753
|
+
const env = options.env ?? process.env;
|
|
754
|
+
if (env.DATABASE_URL) return openPostgres(env.DATABASE_URL, options);
|
|
755
|
+
return openSqlite(options.path ?? defaultIndexPath(env), options);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// ../store/src/derive.ts
|
|
759
|
+
var nodeId = {
|
|
760
|
+
family: (inventory, id, project = "") => `family:${shortHash(inventory)}/${encodeURIComponent(project)}/${id}`,
|
|
761
|
+
file: (project, file) => `file:${project}:${file}`,
|
|
762
|
+
route: (project, route) => `route:${project}:${route}`,
|
|
763
|
+
project: (id) => `project:${id}`,
|
|
764
|
+
library: (name) => `library:${name}`,
|
|
765
|
+
candidate: (id) => `candidate:${id}`,
|
|
766
|
+
rule: (id, project = "") => `rule:${encodeURIComponent(project)}:${id}`,
|
|
767
|
+
component: (id, project = "") => `component:${encodeURIComponent(project)}:${id}`
|
|
768
|
+
};
|
|
769
|
+
var GraphBuilder = class {
|
|
770
|
+
nodes = /* @__PURE__ */ new Map();
|
|
771
|
+
edges = /* @__PURE__ */ new Map();
|
|
772
|
+
node(node) {
|
|
773
|
+
if (!this.nodes.has(node.id)) this.nodes.set(node.id, node);
|
|
774
|
+
return node.id;
|
|
775
|
+
}
|
|
776
|
+
edge(from, to, kind) {
|
|
777
|
+
if (from !== to) this.edges.set(`${from}\0${to}\0${kind}`, { from, to, kind });
|
|
778
|
+
}
|
|
779
|
+
build() {
|
|
780
|
+
return { nodes: [...this.nodes.values()], edges: [...this.edges.values()] };
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
var TOLERANCE_KEY = { height: "height", paddingX: "padding", paddingY: "padding", radius: "radius", fontSize: "fontSize", fontWeight: "fontWeight" };
|
|
784
|
+
function nearDuplicatePairs(families, tolerance) {
|
|
785
|
+
const pairs = [];
|
|
786
|
+
for (let i = 0; i < families.length; i++) {
|
|
787
|
+
for (let j = i + 1; j < families.length; j++) {
|
|
788
|
+
const a = families[i];
|
|
789
|
+
const b = families[j];
|
|
790
|
+
if (a.role !== b.role) continue;
|
|
791
|
+
if (a.kits.length > 0 && b.kits.length > 0 && a.kits.every((k) => b.kits.includes(k)) && b.kits.every((k) => a.kits.includes(k))) continue;
|
|
792
|
+
let compared = 0;
|
|
793
|
+
let close = true;
|
|
794
|
+
for (const dimension of SHAPE_DIMENSIONS) {
|
|
795
|
+
const x = a.shape[dimension];
|
|
796
|
+
const y = b.shape[dimension];
|
|
797
|
+
if (x === null || y === null) continue;
|
|
798
|
+
compared++;
|
|
799
|
+
if (Math.abs(x - y) > 2 * tolerance[TOLERANCE_KEY[dimension]]) close = false;
|
|
800
|
+
}
|
|
801
|
+
if (close && compared >= 2) pairs.push([a.id, b.id]);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return pairs;
|
|
805
|
+
}
|
|
806
|
+
function inventoryRun(report, source) {
|
|
807
|
+
const run = `${report.generatedAt}#${shortHash(`${source}\0${report.generatedAt}`).slice(0, 8)}`;
|
|
808
|
+
const g = new GraphBuilder();
|
|
809
|
+
const familyNode = (id, project) => g.node({ id: nodeId.family(source, id, project), kind: "family", ref: id, project });
|
|
810
|
+
for (const family of report.families) for (const project of family.projects) familyNode(family.id, project);
|
|
811
|
+
const uses = report.looks.filter((l) => !l.story);
|
|
812
|
+
for (const look of uses) {
|
|
813
|
+
if (!look.family) continue;
|
|
814
|
+
const file = g.node({ id: nodeId.file(look.project, look.file), kind: "file", ref: `${look.project}:${look.file}` });
|
|
815
|
+
g.edge(familyNode(look.family, look.project), file, "used-in");
|
|
816
|
+
for (const route of look.routes) g.edge(file, g.node({ id: nodeId.route(look.project, route), kind: "route", ref: `${look.project}:${route}` }), "used-in");
|
|
817
|
+
}
|
|
818
|
+
for (const [a, b] of nearDuplicatePairs(report.families, report.tolerance)) {
|
|
819
|
+
const projects = report.families.find((f) => f.id === a).projects.filter((p) => report.families.find((f) => f.id === b).projects.includes(p));
|
|
820
|
+
for (const project of projects) {
|
|
821
|
+
g.edge(familyNode(a, project), familyNode(b, project), "near-duplicate-of");
|
|
822
|
+
g.edge(familyNode(b, project), familyNode(a, project), "near-duplicate-of");
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
for (const primitive of report.primitives) g.edge(g.node({ id: nodeId.project(primitive.project), kind: "project", ref: primitive.project }), g.node({ id: nodeId.library(primitive.package), kind: "library", ref: primitive.package }), "uses-library");
|
|
826
|
+
for (const kit of report.kits) g.edge(g.node({ id: nodeId.project(kit.project), kind: "project", ref: kit.project }), g.node({ id: nodeId.library(kit.id), kind: "library", ref: kit.id }), "uses-library");
|
|
827
|
+
const roles = (project) => {
|
|
828
|
+
const counts = {};
|
|
829
|
+
for (const look of uses) if (look.project === project) counts[look.role] = (counts[look.role] ?? 0) + 1;
|
|
830
|
+
return counts;
|
|
831
|
+
};
|
|
832
|
+
return {
|
|
833
|
+
run,
|
|
834
|
+
source,
|
|
835
|
+
scannedAt: report.generatedAt,
|
|
836
|
+
projects: report.projects.map((p) => {
|
|
837
|
+
const families = report.families.filter((f) => f.projects.includes(p.id));
|
|
838
|
+
return { project: p.id, root: p.root, commit: p.commit, status: p.status, files: p.files, looks: uses.filter((l) => l.project === p.id).length, families: families.length, nearDuplicates: families.filter((f) => f.nearDuplicate).length, roles: roles(p.id) };
|
|
839
|
+
}),
|
|
840
|
+
families: report.families.map((f) => ({ id: f.id, role: f.role, looks: f.looks, nearDuplicate: f.nearDuplicate, projects: f.projects, kits: f.kits, shape: f.shape })),
|
|
841
|
+
looks: uses.map((l) => ({ id: l.id, project: l.project, role: l.role, file: l.file, line: l.line, column: l.column, tag: l.tag, kit: l.kit, family: l.family, routes: l.routes, shape: l.shape })),
|
|
842
|
+
graph: g.build()
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function strings(value) {
|
|
846
|
+
if (typeof value === "string") return [value];
|
|
847
|
+
if (Array.isArray(value)) return value.flatMap(strings);
|
|
848
|
+
if (value && typeof value === "object") return Object.values(value).flatMap(strings);
|
|
849
|
+
return [];
|
|
850
|
+
}
|
|
851
|
+
function governedComponents(rule, components) {
|
|
852
|
+
if (rule.severity === "off") return [];
|
|
853
|
+
if (rule.check.engine === "youi" && (rule.check.id === "prop-values" || rule.check.id === "lookalike-component")) return components.map((c) => c.id);
|
|
854
|
+
const named = new Set(rule.check.engine === "youi" ? strings(rule.check.options) : []);
|
|
855
|
+
const words2 = `${rule.title} ${rule.statement} ${rule.agent}`;
|
|
856
|
+
return components.filter((c) => named.has(c.id) || new RegExp(`(?:^|[^A-Za-z0-9])<?${c.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:$|[^A-Za-z0-9])`).test(words2)).map((c) => c.id);
|
|
857
|
+
}
|
|
858
|
+
function projectGraph(input) {
|
|
859
|
+
const g = new GraphBuilder();
|
|
860
|
+
const names = new Map(input.components.map((c) => [c.id, c.name]));
|
|
861
|
+
const component = (id) => g.node({ id: nodeId.component(id, input.project), kind: "component", ref: names.get(id) ?? id, project: input.project });
|
|
862
|
+
for (const rule of input.rules) {
|
|
863
|
+
const node = g.node({ id: nodeId.rule(rule.id, input.project), kind: "rule", ref: rule.id, project: input.project });
|
|
864
|
+
for (const id of governedComponents(rule, input.components)) g.edge(node, component(id), "governs");
|
|
865
|
+
}
|
|
866
|
+
for (const site of input.usage?.usages ?? []) {
|
|
867
|
+
const file = g.node({ id: nodeId.file(input.project, site.file), kind: "file", ref: `${input.project}:${site.file}` });
|
|
868
|
+
g.edge(component(site.element), file, "used-in");
|
|
869
|
+
for (const route of site.routes) g.edge(file, g.node({ id: nodeId.route(input.project, route), kind: "route", ref: `${input.project}:${route}` }), "used-in");
|
|
870
|
+
}
|
|
871
|
+
return g.build();
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
export {
|
|
875
|
+
searchTerms,
|
|
876
|
+
numbered,
|
|
877
|
+
Lock,
|
|
878
|
+
pageInteger,
|
|
879
|
+
MIGRATIONS,
|
|
880
|
+
SCHEMA_VERSION,
|
|
881
|
+
SqlStore,
|
|
882
|
+
sqliteAdapter,
|
|
883
|
+
openSqlite,
|
|
884
|
+
checkpointSqlite,
|
|
885
|
+
postgresAdapter,
|
|
886
|
+
redactUrl,
|
|
887
|
+
openPostgres,
|
|
888
|
+
openPglite,
|
|
889
|
+
INDEX_FILE,
|
|
890
|
+
defaultIndexPath,
|
|
891
|
+
indexHome,
|
|
892
|
+
storeLocation,
|
|
893
|
+
openStore,
|
|
894
|
+
nodeId,
|
|
895
|
+
nearDuplicatePairs,
|
|
896
|
+
inventoryRun,
|
|
897
|
+
governedComponents,
|
|
898
|
+
projectGraph
|
|
899
|
+
};
|