openworkgraph 0.3.1 → 0.3.2

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/README.md CHANGED
@@ -1,64 +1,64 @@
1
- # OpenWorkgraph
2
-
3
- OpenWorkgraph 是基于无限画布思想的软件开发工作流产品,同时面向其他领域的工作组织与协作。
4
-
5
- ## 快速开始
6
-
7
- 打开 [OpenWorkgraph 官方编辑器](https://workgraph.giarld.com/),按照页面提供的引导完成配置即可开始使用。
8
-
9
- ## 本地构建与运行
10
-
11
- ### Agent Service
12
-
13
- 在仓库根目录构建协议包和 Agent Service:
14
-
15
- ```sh
16
- npm run build
17
- ```
18
-
19
- 构建完成后,以前台方式运行 Agent Service:
20
-
21
- ```sh
22
- node agent-service/dist/cli.js start
23
- ```
24
-
25
- ### Web Client
26
-
27
- 在仓库根目录构建 Web Client,并通过 Vite Preview 在 `http://127.0.0.1:4173` 运行生产构建:
28
-
29
- ```sh
30
- npm --prefix web-client run build
31
- npm --prefix web-client run preview -- --host 127.0.0.1 --port 4173
32
- ```
33
-
34
- 打开 Web Client 后,填写运行时地址并点击“生成客户端码”。在运行时所在机器执行以下命令,输入浏览器显示的 8 位客户端码,核对来源和公钥指纹后确认;浏览器会轮询批准状态,完成私钥证明和通讯凭据交换后自动连接:
35
-
36
- ```sh
37
- node agent-service/dist/cli.js pair --client-code '<客户端码>'
38
- ```
39
-
40
- 客户端码显示为连续 8 位数字,例如 `12345678`,中间没有空格,5 分钟有效。终端默认输出英文可读的批准结果;仅脚本需要解析时加 `--output-json`(非交互批准还需 `--yes`)。
41
-
42
- 一机只运行一个服务。`--data-dir` 仅用于 `start` 和 `serve`;启动成功后自动记住目录,配对、查看状态、停止、日志和备份命令都无需再填写。停服后运行 `start` 会沿用原目录,首次启动未指定时使用用户主目录下的 `.openworkgraph`。重复启动会明确拒绝,不提供服务选择或切换步骤。
43
-
44
- 无需手填 `--origin`,不强制 HTTPS,支持可信局域网普通 HTTP。HTTP 不加密,请勿暴露公网。
45
-
46
- ### 检查与测试
47
-
48
- ```sh
49
- npm test
50
- npm --prefix web-client run typecheck
51
- npm --prefix web-client test
52
- npm --prefix web-client run build
53
- npm --prefix web-client run test:e2e
54
- ```
55
-
56
- 以上命令在仓库根目录执行;Web 构建输出为 `web-client/dist/`,浏览器测试使用已安装的 Chrome。
57
-
58
- ## 参考依据与致谢
59
-
60
- Web UI 参考了 [basketikun/infinite-canvas](https://github.com/basketikun/infinite-canvas) 的视觉风格和画布交互,感谢原作者 basketikun。该项目采用 [MIT License](https://github.com/basketikun/infinite-canvas/blob/d213a74614e0e4bd8a26383d1e1e907249e9c61b/LICENSE);复用其代码或其他适用内容时,应保留原版权与许可声明。第三方素材若有独立授权,以其各自许可为准。
61
-
62
- ## 许可证
63
-
64
- 本项目采用 [MIT License](LICENSE)。第三方来源的版权与许可声明按原始归属保留。
1
+ # OpenWorkgraph
2
+
3
+ OpenWorkgraph 是基于无限画布思想的软件开发工作流产品,同时面向其他领域的工作组织与协作。
4
+
5
+ ## 快速开始
6
+
7
+ 打开 [OpenWorkgraph 官方编辑器](https://workgraph.giarld.com/),按照页面提供的引导完成配置即可开始使用。
8
+
9
+ ## 本地构建与运行
10
+
11
+ ### Agent Service
12
+
13
+ 在仓库根目录构建协议包和 Agent Service:
14
+
15
+ ```sh
16
+ npm run build
17
+ ```
18
+
19
+ 构建完成后,以前台方式运行 Agent Service:
20
+
21
+ ```sh
22
+ node agent-service/dist/cli.js start
23
+ ```
24
+
25
+ ### Web Client
26
+
27
+ 在仓库根目录构建 Web Client,并通过 Vite Preview 在 `http://127.0.0.1:4173` 运行生产构建:
28
+
29
+ ```sh
30
+ npm --prefix web-client run build
31
+ npm --prefix web-client run preview -- --host 127.0.0.1 --port 4173
32
+ ```
33
+
34
+ 打开 Web Client 后,填写运行时地址并点击“生成客户端码”。在运行时所在机器执行以下命令,输入浏览器显示的 8 位客户端码,核对来源和公钥指纹后确认;浏览器会轮询批准状态,完成私钥证明和通讯凭据交换后自动连接:
35
+
36
+ ```sh
37
+ node agent-service/dist/cli.js pair --client-code '<客户端码>'
38
+ ```
39
+
40
+ 客户端码显示为连续 8 位数字,例如 `12345678`,中间没有空格,5 分钟有效。终端默认输出英文可读的批准结果;仅脚本需要解析时加 `--output-json`(非交互批准还需 `--yes`)。
41
+
42
+ 一机只运行一个服务。`--data-dir` 仅用于 `start` 和 `serve`;启动成功后自动记住目录,配对、查看状态、停止、日志和备份命令都无需再填写。停服后运行 `start` 会沿用原目录,首次启动未指定时使用用户主目录下的 `.openworkgraph`。重复启动会明确拒绝,不提供服务选择或切换步骤。
43
+
44
+ 无需手填 `--origin`,不强制 HTTPS,支持可信局域网普通 HTTP。HTTP 不加密,请勿暴露公网。
45
+
46
+ ### 检查与测试
47
+
48
+ ```sh
49
+ npm test
50
+ npm --prefix web-client run typecheck
51
+ npm --prefix web-client test
52
+ npm --prefix web-client run build
53
+ npm --prefix web-client run test:e2e
54
+ ```
55
+
56
+ 以上命令在仓库根目录执行;Web 构建输出为 `web-client/dist/`,浏览器测试使用已安装的 Chrome。
57
+
58
+ ## 参考依据与致谢
59
+
60
+ Web UI 参考了 [basketikun/infinite-canvas](https://github.com/basketikun/infinite-canvas) 的视觉风格和画布交互,感谢原作者 basketikun。该项目采用 [MIT License](https://github.com/basketikun/infinite-canvas/blob/d213a74614e0e4bd8a26383d1e1e907249e9c61b/LICENSE);复用其代码或其他适用内容时,应保留原版权与许可声明。第三方素材若有独立授权,以其各自许可为准。
61
+
62
+ ## 许可证
63
+
64
+ 本项目采用 [MIT License](LICENSE)。第三方来源的版权与许可声明按原始归属保留。
@@ -35,12 +35,20 @@ export declare class BuiltinSkills {
35
35
  private cached;
36
36
  catalog(refresh?: boolean): Promise<SkillCatalog>;
37
37
  installed(): Promise<SkillCatalogItem[]>;
38
+ /** Local records plus already-known update information; never refresh the source. */
39
+ installedCatalog(): Promise<SkillCatalog>;
38
40
  private versionDirectory;
41
+ /** UI reads only the installed schema and manifest, without hashing package contents. */
42
+ packageMetadata(skillId: string, packageVersion?: string): Promise<{
43
+ config: SkillConfigSchema;
44
+ item: SkillCatalogItem;
45
+ }>;
39
46
  packageEntry(skillId: string, packageVersion?: string): Promise<{
40
47
  path: string;
41
48
  config: SkillConfigSchema;
42
49
  item: SkillCatalogItem;
43
50
  }>;
51
+ private readPackageEntry;
44
52
  /** Holds the same cross-process lock as updates while a configuration write uses its schema. */
45
53
  withPackageEntry<T>(skillId: string, expectedVersion: string, work: (entry: {
46
54
  path: string;
@@ -343,12 +343,27 @@ export class BuiltinSkills {
343
343
  return { items: [...result.values()].sort((a, b) => a.name.localeCompare(b.name)), stale: !!error, ...(error ? { error } : {}) };
344
344
  }
345
345
  async installed() { await this.initialize(); return Object.values((await this.state()).items).filter(item => item.installed); }
346
+ /** Local records plus already-known update information; never refresh the source. */
347
+ async installedCatalog() {
348
+ const items = await this.installed();
349
+ const cache = await this.readJson('catalog-' + key(JSON.stringify(this.source)) + '.json');
350
+ const versions = new Map((cache?.packages ?? []).map(pkg => [pkg.item.skillId, pkg.item.packageVersion]));
351
+ return { items: items.map(item => ({ ...item, updateAvailable: versions.has(item.skillId) && versions.get(item.skillId) !== item.packageVersion })).sort((a, b) => a.name.localeCompare(b.name)), stale: false };
352
+ }
346
353
  versionDirectory(id, version) {
347
354
  if (!id || id.length > 1024 || !/^[a-f0-9]{64}$/.test(version))
348
355
  throw new ServiceError('INVALID_REQUEST', '技能身份或包版本无效。');
349
356
  return 'versions/' + key(id) + '/' + version;
350
357
  }
358
+ /** UI reads only the installed schema and manifest, without hashing package contents. */
359
+ async packageMetadata(skillId, packageVersion) {
360
+ const { config, item } = await this.readPackageEntry(skillId, packageVersion, false);
361
+ return { config, item };
362
+ }
351
363
  async packageEntry(skillId, packageVersion) {
364
+ return this.readPackageEntry(skillId, packageVersion, false);
365
+ }
366
+ async readPackageEntry(skillId, packageVersion, verifyIntegrity) {
352
367
  await this.initialize();
353
368
  const current = (await this.state()).items[skillId];
354
369
  const version = packageVersion ?? (current?.installed ? current.packageVersion : undefined);
@@ -359,13 +374,15 @@ export class BuiltinSkills {
359
374
  if (!manifest || manifest.version !== 1 || manifest.item.skillId !== skillId || manifest.item.packageVersion !== version)
360
375
  throw new ServiceError('NOT_FOUND', '技能固定包版本不可用。');
361
376
  validatePackageFiles({ directory: manifest.item.directory, files: manifest.files });
362
- if (packageDigest(manifest.files) !== version)
363
- throw new ServiceError('INVALID_REQUEST', '技能包元数据摘要无效。');
364
- for (const file of manifest.files) {
365
- const path = await this.checked(relative + '/package/' + file.path);
366
- const stat = await fs.stat(path);
367
- if (!stat.isFile() || stat.size !== file.size || gitBlobSha(await fs.readFile(path)) !== file.sha)
368
- throw new ServiceError('INVALID_REQUEST', '已安装技能包完整性校验失败。');
377
+ if (verifyIntegrity) {
378
+ if (packageDigest(manifest.files) !== version)
379
+ throw new ServiceError('INVALID_REQUEST', '技能包元数据摘要无效。');
380
+ for (const file of manifest.files) {
381
+ const path = await this.checked(relative + '/package/' + file.path);
382
+ const stat = await fs.stat(path);
383
+ if (!stat.isFile() || stat.size !== file.size || gitBlobSha(await fs.readFile(path)) !== file.sha)
384
+ throw new ServiceError('INVALID_REQUEST', '已安装技能包完整性校验失败。');
385
+ }
369
386
  }
370
387
  const config = parseSkillConfig(await fs.readFile(await this.checked(relative + '/package/config.json'), 'utf8'));
371
388
  return { path: await this.checked(relative + '/package/SKILL.md'), config, item: current?.installed && current.packageVersion === version ? current : manifest.item };
@@ -389,18 +406,18 @@ export class BuiltinSkills {
389
406
  if (manifest.version !== 1 || manifest.item.skillId !== skillId || manifest.item.packageVersion !== packageVersion)
390
407
  throw new ServiceError('INVALID_REQUEST', '技能包元数据无效。');
391
408
  validatePackageFiles({ directory: manifest.item.directory, files: manifest.files });
392
- if (packageDigest(manifest.files) !== packageVersion)
393
- throw new ServiceError('INVALID_REQUEST', '技能包元数据摘要无效。');
394
409
  const file = manifest.files.find(file => file.path === path);
395
410
  if (!file)
396
411
  throw new ServiceError('NOT_FOUND', '技能包资源不存在。');
397
412
  const target = await this.checked(relative + '/package/' + path);
398
413
  const stat = await fs.stat(target);
399
- if (!stat.isFile() || stat.size !== file.size)
400
- throw new ServiceError('INVALID_REQUEST', '技能资源完整性校验失败。');
414
+ if (!stat.isFile())
415
+ throw new ServiceError('INVALID_REQUEST', '技能资源不是普通文件。');
416
+ if (stat.size > SKILL_LIMITS.fileBytes)
417
+ throw new ServiceError('PAYLOAD_TOO_LARGE', '技能资源超过大小限制。');
401
418
  const bytes = await fs.readFile(target);
402
- if (gitBlobSha(bytes) !== file.sha)
403
- throw new ServiceError('INVALID_REQUEST', '技能资源完整性校验失败。');
419
+ if (bytes.length > SKILL_LIMITS.fileBytes)
420
+ throw new ServiceError('PAYLOAD_TOO_LARGE', '技能资源超过大小限制。');
404
421
  return bytes;
405
422
  }
406
423
  const { cache, error } = await this.cached();
@@ -478,7 +495,7 @@ export class BuiltinSkills {
478
495
  if (!existing)
479
496
  await fs.rename(await this.checked(staging), await this.checked(target));
480
497
  else
481
- await this.packageEntry(skillId, item.packageVersion);
498
+ await this.readPackageEntry(skillId, item.packageVersion, true);
482
499
  deadline.check();
483
500
  state.items[skillId] = item;
484
501
  await this.atomicJson('installed.json', state);
package/dist/cli.js CHANGED
File without changes
package/dist/graphs.js CHANGED
@@ -188,19 +188,19 @@ export class Graphs {
188
188
  activeNode(nodeId) { return Boolean(this.db.prepare(`SELECT 1 FROM runs WHERE node_id=? AND status NOT IN (${TERMINAL_RUN_STATUSES.map(() => '?').join(',')}) LIMIT 1`).get(nodeId, ...TERMINAL_RUN_STATUSES)); }
189
189
  activeExecutionChainNodeIds(scope) {
190
190
  const terminal = TERMINAL_RUN_STATUSES.map(() => '?').join(',');
191
- const rows = this.db.prepare(`
192
- SELECT DISTINCT member.node_id
193
- FROM runs member
194
- JOIN run_runtime member_rt ON member_rt.run_id=member.id
195
- WHERE member.graph_id=?
196
- AND json_extract(member_rt.details,'$.chainBatch') IN (
197
- SELECT json_extract(active_rt.details,'$.chainBatch')
198
- FROM runs active
199
- JOIN run_runtime active_rt ON active_rt.run_id=active.id
200
- WHERE active.graph_id=?
201
- AND active.status NOT IN (${terminal})
202
- AND json_extract(active_rt.details,'$.chainBatch') IS NOT NULL
203
- )
191
+ const rows = this.db.prepare(`
192
+ SELECT DISTINCT member.node_id
193
+ FROM runs member
194
+ JOIN run_runtime member_rt ON member_rt.run_id=member.id
195
+ WHERE member.graph_id=?
196
+ AND json_extract(member_rt.details,'$.chainBatch') IN (
197
+ SELECT json_extract(active_rt.details,'$.chainBatch')
198
+ FROM runs active
199
+ JOIN run_runtime active_rt ON active_rt.run_id=active.id
200
+ WHERE active.graph_id=?
201
+ AND active.status NOT IN (${terminal})
202
+ AND json_extract(active_rt.details,'$.chainBatch') IS NOT NULL
203
+ )
204
204
  `).all(scope.graphId, scope.graphId, ...TERMINAL_RUN_STATUSES);
205
205
  return new Set(rows.map(row => String(row['node_id'])));
206
206
  }
@@ -271,10 +271,10 @@ export class Graphs {
271
271
  const hashes = this.db.prepare('SELECT DISTINCT v.sha256 FROM canvas_resource_versions v JOIN canvas_resources r ON r.id=v.resource_id WHERE r.graph_id=?').all(id).map(row => String(row['sha256']));
272
272
  // Keep request hashes as non-replayable tombstones: retries cannot recreate
273
273
  // a deleted graph or disclose old snapshots through cached responses.
274
- this.db.prepare(`UPDATE idempotency SET response='null',invalidated=1 WHERE
275
- json_extract(response,'$.graphId')=? OR json_extract(response,'$.resource.graphId')=?
276
- OR json_extract(response,'$.runId') IN (SELECT id FROM runs WHERE graph_id=?)
277
- OR json_extract(response,'$.id') IN (SELECT id FROM runs WHERE graph_id=?)
274
+ this.db.prepare(`UPDATE idempotency SET response='null',invalidated=1 WHERE
275
+ json_extract(response,'$.graphId')=? OR json_extract(response,'$.resource.graphId')=?
276
+ OR json_extract(response,'$.runId') IN (SELECT id FROM runs WHERE graph_id=?)
277
+ OR json_extract(response,'$.id') IN (SELECT id FROM runs WHERE graph_id=?)
278
278
  OR EXISTS(SELECT 1 FROM json_each(response,'$.runIds') j WHERE j.value IN (SELECT id FROM runs WHERE graph_id=?))`).run(id, id, id, id, id);
279
279
  this.db.prepare('DELETE FROM asset_references WHERE run_id IN (SELECT id FROM runs WHERE graph_id=?) OR node_id IN (SELECT id FROM nodes WHERE graph_id=?)').run(id, id);
280
280
  this.db.prepare('DELETE FROM canvas_resource_references WHERE graph_id=?').run(id);
@@ -1,36 +1,36 @@
1
1
  /** Additive schema 6 fragment. No legacy resource conversion or file migration. */
2
- export const ASSET_BUSINESS_SCHEMA = `
3
- CREATE TABLE resource_uploads (
4
- id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id),
5
- name TEXT NOT NULL, mime TEXT NOT NULL, bytes INTEGER NOT NULL CHECK(bytes >= 0),
6
- expected_sha256 TEXT, received INTEGER NOT NULL DEFAULT 0 CHECK(received >= 0 AND received <= bytes),
7
- state TEXT NOT NULL DEFAULT 'uploading' CHECK(state IN ('uploading','finished')),
8
- sha256 TEXT REFERENCES blobs(sha256), created_at INTEGER NOT NULL,
9
- CHECK((state='finished' AND sha256 IS NOT NULL) OR (state='uploading' AND sha256 IS NULL))
10
- ) STRICT;
11
- CREATE TABLE resource_upload_chunks (
12
- upload_id TEXT NOT NULL REFERENCES resource_uploads(id), offset INTEGER NOT NULL CHECK(offset >= 0),
13
- bytes INTEGER NOT NULL CHECK(bytes > 0), sha256 TEXT NOT NULL, path TEXT NOT NULL UNIQUE,
14
- PRIMARY KEY(upload_id,offset)
15
- ) STRICT;
16
- CREATE TABLE resource_blob_files (sha256 TEXT PRIMARY KEY REFERENCES blobs(sha256), path TEXT NOT NULL UNIQUE) STRICT;
17
- CREATE TABLE resource_file_deletions (path TEXT PRIMARY KEY, created_at INTEGER NOT NULL, completed INTEGER NOT NULL DEFAULT 0 CHECK(completed IN (0,1))) STRICT;
18
- CREATE TABLE resource_read_leases (id TEXT PRIMARY KEY, sha256 TEXT NOT NULL REFERENCES blobs(sha256)) STRICT;
19
- CREATE TABLE resource_maintenance_leases (id TEXT PRIMARY KEY, kind TEXT NOT NULL CHECK(kind IN ('backup','drain')), created_at INTEGER NOT NULL) STRICT;
20
- CREATE TABLE resource_representations (
21
- sha256 TEXT NOT NULL REFERENCES blobs(sha256), processor TEXT NOT NULL, version INTEGER NOT NULL CHECK(version > 0),
22
- state TEXT NOT NULL CHECK(state IN ('ready','unsupported','failed')), mime TEXT NOT NULL, text TEXT, reason TEXT,
23
- PRIMARY KEY(sha256,processor,version)
24
- ) STRICT;
25
- CREATE TRIGGER resource_blob_file_immutable BEFORE UPDATE ON resource_blob_files BEGIN SELECT RAISE(ABORT,'immutable blob file'); END;
26
- CREATE TRIGGER resource_asset_ref_reset AFTER UPDATE OF asset_id ON asset_references BEGIN
27
- UPDATE assets SET unreferenced_since=NULL WHERE id IN (OLD.asset_id,NEW.asset_id);
28
- END;
29
- CREATE TRIGGER resource_no_rebind_deleted_asset BEFORE UPDATE OF asset_id,asset_version ON asset_references
30
- WHEN (NEW.asset_id!=OLD.asset_id OR NEW.asset_version!=OLD.asset_version) AND (SELECT deleted_at FROM assets WHERE id=NEW.asset_id) IS NOT NULL
31
- BEGIN SELECT RAISE(ABORT,'deleted assets reject new references'); END;
32
- CREATE TRIGGER resource_output_added AFTER INSERT ON outputs BEGIN UPDATE assets SET unreferenced_since=NULL WHERE id=NEW.asset_id; END;
33
- CREATE TRIGGER resource_output_removed AFTER DELETE ON outputs BEGIN UPDATE assets SET unreferenced_since=NULL WHERE id=OLD.asset_id; END;
34
- CREATE TRIGGER resource_output_changed AFTER UPDATE OF asset_id ON outputs BEGIN UPDATE assets SET unreferenced_since=NULL WHERE id IN (OLD.asset_id,NEW.asset_id); END;
2
+ export const ASSET_BUSINESS_SCHEMA = `
3
+ CREATE TABLE resource_uploads (
4
+ id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id),
5
+ name TEXT NOT NULL, mime TEXT NOT NULL, bytes INTEGER NOT NULL CHECK(bytes >= 0),
6
+ expected_sha256 TEXT, received INTEGER NOT NULL DEFAULT 0 CHECK(received >= 0 AND received <= bytes),
7
+ state TEXT NOT NULL DEFAULT 'uploading' CHECK(state IN ('uploading','finished')),
8
+ sha256 TEXT REFERENCES blobs(sha256), created_at INTEGER NOT NULL,
9
+ CHECK((state='finished' AND sha256 IS NOT NULL) OR (state='uploading' AND sha256 IS NULL))
10
+ ) STRICT;
11
+ CREATE TABLE resource_upload_chunks (
12
+ upload_id TEXT NOT NULL REFERENCES resource_uploads(id), offset INTEGER NOT NULL CHECK(offset >= 0),
13
+ bytes INTEGER NOT NULL CHECK(bytes > 0), sha256 TEXT NOT NULL, path TEXT NOT NULL UNIQUE,
14
+ PRIMARY KEY(upload_id,offset)
15
+ ) STRICT;
16
+ CREATE TABLE resource_blob_files (sha256 TEXT PRIMARY KEY REFERENCES blobs(sha256), path TEXT NOT NULL UNIQUE) STRICT;
17
+ CREATE TABLE resource_file_deletions (path TEXT PRIMARY KEY, created_at INTEGER NOT NULL, completed INTEGER NOT NULL DEFAULT 0 CHECK(completed IN (0,1))) STRICT;
18
+ CREATE TABLE resource_read_leases (id TEXT PRIMARY KEY, sha256 TEXT NOT NULL REFERENCES blobs(sha256)) STRICT;
19
+ CREATE TABLE resource_maintenance_leases (id TEXT PRIMARY KEY, kind TEXT NOT NULL CHECK(kind IN ('backup','drain')), created_at INTEGER NOT NULL) STRICT;
20
+ CREATE TABLE resource_representations (
21
+ sha256 TEXT NOT NULL REFERENCES blobs(sha256), processor TEXT NOT NULL, version INTEGER NOT NULL CHECK(version > 0),
22
+ state TEXT NOT NULL CHECK(state IN ('ready','unsupported','failed')), mime TEXT NOT NULL, text TEXT, reason TEXT,
23
+ PRIMARY KEY(sha256,processor,version)
24
+ ) STRICT;
25
+ CREATE TRIGGER resource_blob_file_immutable BEFORE UPDATE ON resource_blob_files BEGIN SELECT RAISE(ABORT,'immutable blob file'); END;
26
+ CREATE TRIGGER resource_asset_ref_reset AFTER UPDATE OF asset_id ON asset_references BEGIN
27
+ UPDATE assets SET unreferenced_since=NULL WHERE id IN (OLD.asset_id,NEW.asset_id);
28
+ END;
29
+ CREATE TRIGGER resource_no_rebind_deleted_asset BEFORE UPDATE OF asset_id,asset_version ON asset_references
30
+ WHEN (NEW.asset_id!=OLD.asset_id OR NEW.asset_version!=OLD.asset_version) AND (SELECT deleted_at FROM assets WHERE id=NEW.asset_id) IS NOT NULL
31
+ BEGIN SELECT RAISE(ABORT,'deleted assets reject new references'); END;
32
+ CREATE TRIGGER resource_output_added AFTER INSERT ON outputs BEGIN UPDATE assets SET unreferenced_since=NULL WHERE id=NEW.asset_id; END;
33
+ CREATE TRIGGER resource_output_removed AFTER DELETE ON outputs BEGIN UPDATE assets SET unreferenced_since=NULL WHERE id=OLD.asset_id; END;
34
+ CREATE TRIGGER resource_output_changed AFTER UPDATE OF asset_id ON outputs BEGIN UPDATE assets SET unreferenced_since=NULL WHERE id IN (OLD.asset_id,NEW.asset_id); END;
35
35
  `;
36
36
  //# sourceMappingURL=asset-business-schema.js.map
@@ -6,33 +6,33 @@
6
6
  * explicit recovery metadata; this migration cannot infer those mutations.
7
7
  * Ambiguous preexisting read_only rows stay original, never auto-unlocked.
8
8
  */
9
- export const PLUGIN_SCHEMA = `
10
- ALTER TABLE nodes ADD COLUMN read_only_reason TEXT NOT NULL DEFAULT 'none'
11
- CHECK(read_only_reason IN ('none','missing_plugin','original'));
12
- UPDATE nodes SET read_only_reason='original' WHERE read_only=1;
13
- CREATE TABLE node_version_schemas (
14
- node_id TEXT NOT NULL,
15
- version INTEGER NOT NULL CHECK(version > 0),
16
- schema_version INTEGER NOT NULL CHECK(schema_version > 0),
17
- PRIMARY KEY(node_id,version),
18
- FOREIGN KEY(node_id,version) REFERENCES node_versions(node_id,version) ON DELETE CASCADE
19
- ) STRICT;
20
- INSERT INTO node_version_schemas(node_id,version,schema_version)
21
- SELECT v.node_id,v.version,n.schema_version FROM node_versions v JOIN nodes n ON n.id=v.node_id;
22
- CREATE TRIGGER node_version_schema_capture AFTER INSERT ON node_versions BEGIN
23
- INSERT INTO node_version_schemas(node_id,version,schema_version)
24
- SELECT NEW.node_id,NEW.version,schema_version FROM nodes WHERE id=NEW.node_id;
25
- END;
26
- CREATE TRIGGER node_version_schema_immutable BEFORE UPDATE ON node_version_schemas BEGIN
27
- SELECT RAISE(ABORT,'immutable node version schema');
28
- END;
29
- CREATE TRIGGER node_version_schema_delete_guard BEFORE DELETE ON node_version_schemas
30
- WHEN EXISTS(SELECT 1 FROM node_versions WHERE node_id=OLD.node_id AND version=OLD.version) BEGIN
31
- SELECT RAISE(ABORT,'retained node version requires schema history');
32
- END;
33
- CREATE TRIGGER node_current_schema_guard BEFORE UPDATE OF schema_version,current_version ON nodes
34
- WHEN EXISTS(SELECT 1 FROM node_version_schemas s WHERE s.node_id=NEW.id AND s.version=NEW.current_version AND s.schema_version!=NEW.schema_version) BEGIN
35
- SELECT RAISE(ABORT,'current node schema does not match version history');
36
- END;
9
+ export const PLUGIN_SCHEMA = `
10
+ ALTER TABLE nodes ADD COLUMN read_only_reason TEXT NOT NULL DEFAULT 'none'
11
+ CHECK(read_only_reason IN ('none','missing_plugin','original'));
12
+ UPDATE nodes SET read_only_reason='original' WHERE read_only=1;
13
+ CREATE TABLE node_version_schemas (
14
+ node_id TEXT NOT NULL,
15
+ version INTEGER NOT NULL CHECK(version > 0),
16
+ schema_version INTEGER NOT NULL CHECK(schema_version > 0),
17
+ PRIMARY KEY(node_id,version),
18
+ FOREIGN KEY(node_id,version) REFERENCES node_versions(node_id,version) ON DELETE CASCADE
19
+ ) STRICT;
20
+ INSERT INTO node_version_schemas(node_id,version,schema_version)
21
+ SELECT v.node_id,v.version,n.schema_version FROM node_versions v JOIN nodes n ON n.id=v.node_id;
22
+ CREATE TRIGGER node_version_schema_capture AFTER INSERT ON node_versions BEGIN
23
+ INSERT INTO node_version_schemas(node_id,version,schema_version)
24
+ SELECT NEW.node_id,NEW.version,schema_version FROM nodes WHERE id=NEW.node_id;
25
+ END;
26
+ CREATE TRIGGER node_version_schema_immutable BEFORE UPDATE ON node_version_schemas BEGIN
27
+ SELECT RAISE(ABORT,'immutable node version schema');
28
+ END;
29
+ CREATE TRIGGER node_version_schema_delete_guard BEFORE DELETE ON node_version_schemas
30
+ WHEN EXISTS(SELECT 1 FROM node_versions WHERE node_id=OLD.node_id AND version=OLD.version) BEGIN
31
+ SELECT RAISE(ABORT,'retained node version requires schema history');
32
+ END;
33
+ CREATE TRIGGER node_current_schema_guard BEFORE UPDATE OF schema_version,current_version ON nodes
34
+ WHEN EXISTS(SELECT 1 FROM node_version_schemas s WHERE s.node_id=NEW.id AND s.version=NEW.current_version AND s.schema_version!=NEW.schema_version) BEGIN
35
+ SELECT RAISE(ABORT,'current node schema does not match version history');
36
+ END;
37
37
  `;
38
38
  //# sourceMappingURL=plugin-schema.js.map
@@ -1,44 +1,44 @@
1
1
  /** Additive only: legacy asset references, outputs and snapshot JSON are preserved. */
2
- export const RESOURCE_SCHEMA = `
3
- ALTER TABLE assets ADD COLUMN name TEXT NOT NULL DEFAULT '';
4
- ALTER TABLE assets ADD COLUMN current_version INTEGER NOT NULL DEFAULT 0 CHECK(current_version >= 0);
5
- UPDATE assets SET current_version=COALESCE((SELECT MAX(version) FROM asset_versions WHERE asset_id=assets.id),0);
6
- CREATE INDEX assets_project_name ON assets(project_id,name);
7
- CREATE UNIQUE INDEX runs_id_graph ON runs(id,graph_id);
8
- CREATE TABLE canvas_resources (
9
- id TEXT PRIMARY KEY, project_id TEXT NOT NULL, graph_id TEXT NOT NULL, name TEXT NOT NULL,
10
- current_version INTEGER NOT NULL CHECK(current_version > 0), UNIQUE(id,graph_id),
11
- FOREIGN KEY(graph_id,project_id) REFERENCES graphs(id,project_id),
12
- FOREIGN KEY(id,current_version) REFERENCES canvas_resource_versions(resource_id,version) DEFERRABLE INITIALLY DEFERRED
13
- ) STRICT;
14
- CREATE TABLE canvas_resource_versions (
15
- resource_id TEXT NOT NULL REFERENCES canvas_resources(id) DEFERRABLE INITIALLY DEFERRED,
16
- version INTEGER NOT NULL CHECK(version > 0), sha256 TEXT NOT NULL REFERENCES blobs(sha256), mime TEXT NOT NULL,
17
- representation_version INTEGER, PRIMARY KEY(resource_id,version)
18
- ) STRICT;
19
- CREATE TRIGGER canvas_version_immutable BEFORE UPDATE ON canvas_resource_versions BEGIN SELECT RAISE(ABORT,'immutable canvas resource version'); END;
20
- CREATE TRIGGER canvas_binding_immutable BEFORE UPDATE OF project_id,graph_id ON canvas_resources WHEN NEW.project_id != OLD.project_id OR NEW.graph_id != OLD.graph_id BEGIN SELECT RAISE(ABORT,'canvas resource binding immutable'); END;
21
- CREATE TABLE canvas_resource_references (
22
- id TEXT PRIMARY KEY, resource_id TEXT NOT NULL, resource_version INTEGER NOT NULL, graph_id TEXT NOT NULL,
23
- owner_kind TEXT NOT NULL CHECK(owner_kind IN ('graph','node','snapshot','history')),
24
- node_id TEXT, run_id TEXT,
25
- CHECK((owner_kind='graph' AND node_id IS NULL AND run_id IS NULL) OR (owner_kind='node' AND node_id IS NOT NULL AND run_id IS NULL) OR (owner_kind IN ('snapshot','history') AND node_id IS NULL AND run_id IS NOT NULL)),
26
- FOREIGN KEY(resource_id,resource_version) REFERENCES canvas_resource_versions(resource_id,version),
27
- FOREIGN KEY(resource_id,graph_id) REFERENCES canvas_resources(id,graph_id),
28
- FOREIGN KEY(node_id,graph_id) REFERENCES nodes(id,graph_id),
29
- FOREIGN KEY(run_id,graph_id) REFERENCES runs(id,graph_id)
30
- ) STRICT;
31
- CREATE INDEX canvas_refs_resource ON canvas_resource_references(resource_id,resource_version);
32
- CREATE TRIGGER frozen_resource_reference_immutable BEFORE UPDATE ON canvas_resource_references WHEN OLD.owner_kind IN ('snapshot','history') BEGIN SELECT RAISE(ABORT,'frozen resource reference'); END;
33
- CREATE TABLE canvas_outputs (
34
- run_id TEXT NOT NULL, graph_id TEXT NOT NULL, output_key TEXT NOT NULL, resource_id TEXT NOT NULL, resource_version INTEGER NOT NULL,
35
- PRIMARY KEY(run_id,output_key), FOREIGN KEY(resource_id,resource_version) REFERENCES canvas_resource_versions(resource_id,version),
36
- FOREIGN KEY(resource_id,graph_id) REFERENCES canvas_resources(id,graph_id), FOREIGN KEY(run_id,graph_id) REFERENCES runs(id,graph_id)
37
- ) STRICT;
38
- CREATE TRIGGER no_new_canvas_asset_link BEFORE INSERT ON asset_references WHEN NEW.node_id IS NOT NULL BEGIN SELECT RAISE(ABORT,'canvas nodes must use independent canvas resources'); END;
39
- CREATE TRIGGER no_rebind_canvas_asset_link BEFORE UPDATE OF asset_id,asset_version,node_id ON asset_references WHEN NEW.node_id IS NOT NULL BEGIN SELECT RAISE(ABORT,'legacy canvas asset references cannot be rebound'); END;
40
- CREATE TRIGGER no_new_deleted_asset_reference BEFORE INSERT ON asset_references WHEN (SELECT deleted_at FROM assets WHERE id=NEW.asset_id) IS NOT NULL BEGIN SELECT RAISE(ABORT,'deleted assets reject new references'); END;
41
- CREATE TRIGGER asset_reference_added AFTER INSERT ON asset_references BEGIN UPDATE assets SET unreferenced_since=NULL WHERE id=NEW.asset_id; END;
42
- CREATE TRIGGER asset_reference_removed AFTER DELETE ON asset_references BEGIN UPDATE assets SET unreferenced_since=NULL WHERE id=OLD.asset_id; END;
2
+ export const RESOURCE_SCHEMA = `
3
+ ALTER TABLE assets ADD COLUMN name TEXT NOT NULL DEFAULT '';
4
+ ALTER TABLE assets ADD COLUMN current_version INTEGER NOT NULL DEFAULT 0 CHECK(current_version >= 0);
5
+ UPDATE assets SET current_version=COALESCE((SELECT MAX(version) FROM asset_versions WHERE asset_id=assets.id),0);
6
+ CREATE INDEX assets_project_name ON assets(project_id,name);
7
+ CREATE UNIQUE INDEX runs_id_graph ON runs(id,graph_id);
8
+ CREATE TABLE canvas_resources (
9
+ id TEXT PRIMARY KEY, project_id TEXT NOT NULL, graph_id TEXT NOT NULL, name TEXT NOT NULL,
10
+ current_version INTEGER NOT NULL CHECK(current_version > 0), UNIQUE(id,graph_id),
11
+ FOREIGN KEY(graph_id,project_id) REFERENCES graphs(id,project_id),
12
+ FOREIGN KEY(id,current_version) REFERENCES canvas_resource_versions(resource_id,version) DEFERRABLE INITIALLY DEFERRED
13
+ ) STRICT;
14
+ CREATE TABLE canvas_resource_versions (
15
+ resource_id TEXT NOT NULL REFERENCES canvas_resources(id) DEFERRABLE INITIALLY DEFERRED,
16
+ version INTEGER NOT NULL CHECK(version > 0), sha256 TEXT NOT NULL REFERENCES blobs(sha256), mime TEXT NOT NULL,
17
+ representation_version INTEGER, PRIMARY KEY(resource_id,version)
18
+ ) STRICT;
19
+ CREATE TRIGGER canvas_version_immutable BEFORE UPDATE ON canvas_resource_versions BEGIN SELECT RAISE(ABORT,'immutable canvas resource version'); END;
20
+ CREATE TRIGGER canvas_binding_immutable BEFORE UPDATE OF project_id,graph_id ON canvas_resources WHEN NEW.project_id != OLD.project_id OR NEW.graph_id != OLD.graph_id BEGIN SELECT RAISE(ABORT,'canvas resource binding immutable'); END;
21
+ CREATE TABLE canvas_resource_references (
22
+ id TEXT PRIMARY KEY, resource_id TEXT NOT NULL, resource_version INTEGER NOT NULL, graph_id TEXT NOT NULL,
23
+ owner_kind TEXT NOT NULL CHECK(owner_kind IN ('graph','node','snapshot','history')),
24
+ node_id TEXT, run_id TEXT,
25
+ CHECK((owner_kind='graph' AND node_id IS NULL AND run_id IS NULL) OR (owner_kind='node' AND node_id IS NOT NULL AND run_id IS NULL) OR (owner_kind IN ('snapshot','history') AND node_id IS NULL AND run_id IS NOT NULL)),
26
+ FOREIGN KEY(resource_id,resource_version) REFERENCES canvas_resource_versions(resource_id,version),
27
+ FOREIGN KEY(resource_id,graph_id) REFERENCES canvas_resources(id,graph_id),
28
+ FOREIGN KEY(node_id,graph_id) REFERENCES nodes(id,graph_id),
29
+ FOREIGN KEY(run_id,graph_id) REFERENCES runs(id,graph_id)
30
+ ) STRICT;
31
+ CREATE INDEX canvas_refs_resource ON canvas_resource_references(resource_id,resource_version);
32
+ CREATE TRIGGER frozen_resource_reference_immutable BEFORE UPDATE ON canvas_resource_references WHEN OLD.owner_kind IN ('snapshot','history') BEGIN SELECT RAISE(ABORT,'frozen resource reference'); END;
33
+ CREATE TABLE canvas_outputs (
34
+ run_id TEXT NOT NULL, graph_id TEXT NOT NULL, output_key TEXT NOT NULL, resource_id TEXT NOT NULL, resource_version INTEGER NOT NULL,
35
+ PRIMARY KEY(run_id,output_key), FOREIGN KEY(resource_id,resource_version) REFERENCES canvas_resource_versions(resource_id,version),
36
+ FOREIGN KEY(resource_id,graph_id) REFERENCES canvas_resources(id,graph_id), FOREIGN KEY(run_id,graph_id) REFERENCES runs(id,graph_id)
37
+ ) STRICT;
38
+ CREATE TRIGGER no_new_canvas_asset_link BEFORE INSERT ON asset_references WHEN NEW.node_id IS NOT NULL BEGIN SELECT RAISE(ABORT,'canvas nodes must use independent canvas resources'); END;
39
+ CREATE TRIGGER no_rebind_canvas_asset_link BEFORE UPDATE OF asset_id,asset_version,node_id ON asset_references WHEN NEW.node_id IS NOT NULL BEGIN SELECT RAISE(ABORT,'legacy canvas asset references cannot be rebound'); END;
40
+ CREATE TRIGGER no_new_deleted_asset_reference BEFORE INSERT ON asset_references WHEN (SELECT deleted_at FROM assets WHERE id=NEW.asset_id) IS NOT NULL BEGIN SELECT RAISE(ABORT,'deleted assets reject new references'); END;
41
+ CREATE TRIGGER asset_reference_added AFTER INSERT ON asset_references BEGIN UPDATE assets SET unreferenced_since=NULL WHERE id=NEW.asset_id; END;
42
+ CREATE TRIGGER asset_reference_removed AFTER DELETE ON asset_references BEGIN UPDATE assets SET unreferenced_since=NULL WHERE id=OLD.asset_id; END;
43
43
  `;
44
44
  //# sourceMappingURL=resource-schema.js.map