@wirecat/cli-core 0.372.0 → 0.373.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 CHANGED
@@ -7,6 +7,17 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.373.0 — 20.10.2026
11
+
12
+ ### Added
13
+
14
+ - **Read-only project annotation snapshots from `./store`** preserve project metadata, alias provenance
15
+ and timestamps, facts and direct links in a version-2 manifest. `snapshotProjectAnnotations` reads one
16
+ explicit project from an existing current-schema store in a consistent SQLite transaction, with
17
+ bounded source hashing and no migration or source text in the result. The decoder and fingerprint
18
+ helpers validate that format independently. Notes, meeting memberships, tasks, memories, labels
19
+ and findings remain explicitly omitted; restore and CLI commands are not included.
20
+
10
21
  ## 0.372.0 — 20.10.2026
11
22
 
12
23
  ### Added
@@ -16,6 +16,7 @@ export { openCache } from "./open.js";
16
16
  export { storePath } from "./path.js";
17
17
  export { decodeProjectAnnotations, PROJECT_ANNOTATIONS_MAX_BYTES, type ProjectAnnotations, projectAnnotationsFingerprint, } from "./project-transfer/manifest.js";
18
18
  export { type ProjectAnnotationsPlan, type ProjectAnnotationsTarget, planProjectAnnotations, } from "./project-transfer/plan.js";
19
+ export { decodeProjectAnnotationSnapshot, type ProjectAnnotationSnapshot, type ProjectAnnotationSnapshotOptions, type ProjectAnnotationSnapshotResult, projectAnnotationSnapshotFingerprint, snapshotProjectAnnotations, } from "./project-transfer/snapshot.js";
19
20
  export type { AgentAction, AgentActionInput, AgentActionsStore } from "./sqlite/agent-actions.js";
20
21
  export { resetAttachmentWords } from "./sqlite/attachment-texts.js";
21
22
  export { backfillNormalized, pendingNormalization } from "./sqlite/backfill.js";
@@ -8,6 +8,7 @@ export { openCache } from "./open.js";
8
8
  export { storePath } from "./path.js";
9
9
  export { decodeProjectAnnotations, PROJECT_ANNOTATIONS_MAX_BYTES, projectAnnotationsFingerprint, } from "./project-transfer/manifest.js";
10
10
  export { planProjectAnnotations, } from "./project-transfer/plan.js";
11
+ export { decodeProjectAnnotationSnapshot, projectAnnotationSnapshotFingerprint, snapshotProjectAnnotations, } from "./project-transfer/snapshot.js";
11
12
  export { resetAttachmentWords } from "./sqlite/attachment-texts.js";
12
13
  export { backfillNormalized, pendingNormalization } from "./sqlite/backfill.js";
13
14
  export { CATALOG_QUERY } from "./sqlite/catalog.js";
@@ -1,6 +1,7 @@
1
1
  import { type ProjectAnnotations } from "./schema.js";
2
2
  export type { ProjectAnnotations } from "./schema.js";
3
3
  export declare const PROJECT_ANNOTATIONS_MAX_BYTES: number;
4
+ export declare const validateProjectAnnotationsGraph: (manifest: ProjectAnnotations, additionalActors?: string[]) => void;
4
5
  /** Decode bounded JSON, never a row/column importer or an implicit whole-project backup. */
5
6
  export declare const decodeProjectAnnotations: (json: string) => ProjectAnnotations;
6
7
  export declare const projectAnnotationsFingerprint: (json: string) => string;
@@ -17,7 +17,7 @@ const canonical = (ref) => {
17
17
  return invalid("reference");
18
18
  }
19
19
  };
20
- const graph = (manifest) => {
20
+ export const validateProjectAnnotationsGraph = (manifest, additionalActors = []) => {
21
21
  const owned = new Set([manifest.project.ref]);
22
22
  const external = new Set();
23
23
  const used = new Set();
@@ -59,6 +59,10 @@ const graph = (manifest) => {
59
59
  if (manifest.project.organization !== null)
60
60
  kind(manifest.project.organization, ["organization"]);
61
61
  dependency(manifest.project.organization);
62
+ for (const actor of additionalActors) {
63
+ kind(actor, ["person", "bot"]);
64
+ dependency(actor);
65
+ }
62
66
  const aliases = new Set([manifest.project.name, manifest.project.key].map(normalizeVocabularyName));
63
67
  for (const alias of manifest.aliases) {
64
68
  const name = normalizeVocabularyName(alias);
@@ -121,7 +125,7 @@ export const decodeProjectAnnotations = (json) => {
121
125
  const manifest = parsed.output;
122
126
  if (manifest.aliases.length + manifest.facts.length + manifest.links.length + manifest.external.length > 1000)
123
127
  invalid("record budget");
124
- graph(manifest);
128
+ validateProjectAnnotationsGraph(manifest);
125
129
  return manifest;
126
130
  };
127
131
  export const projectAnnotationsFingerprint = (json) => {
@@ -0,0 +1,118 @@
1
+ import * as v from "valibot";
2
+ import { type ProjectAnnotations } from "./schema.js";
3
+ declare const schema: v.StrictObjectSchema<{
4
+ readonly format: v.LiteralSchema<"wirecat.project-annotations", undefined>;
5
+ readonly facts: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
6
+ readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
7
+ readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
8
+ readonly statement: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
9
+ readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed"], undefined>;
10
+ readonly subject: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
11
+ readonly occurred_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
12
+ readonly supersedes: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
13
+ readonly author: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
14
+ readonly model: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
15
+ readonly confirmed_by: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
16
+ readonly scope: v.NullableSchema<v.PicklistSchema<readonly ["personal", "work"], undefined>, undefined>;
17
+ readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
18
+ readonly created_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
19
+ readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
20
+ }, undefined>, undefined>, v.MaxLengthAction<{
21
+ ref: string;
22
+ type: "decision" | "fact" | "promise" | "question" | "statement";
23
+ statement: string;
24
+ status: "accepted" | "proposed" | "reversed" | "superseded";
25
+ subject: string | null;
26
+ occurred_at: number | null;
27
+ supersedes: string | null;
28
+ author: string;
29
+ model: string | null;
30
+ confirmed_by: string | null;
31
+ scope: "personal" | "work" | null;
32
+ source: string;
33
+ created_at: number;
34
+ updated_at: number;
35
+ }[], 1000, undefined>]>;
36
+ readonly links: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
37
+ readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
38
+ readonly from: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
39
+ readonly to: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
40
+ readonly kind: v.PicklistSchema<readonly ["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to"], undefined>;
41
+ readonly origin: v.PicklistSchema<readonly ["file", "owner", "suggested", "message"], undefined>;
42
+ readonly anchor: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
43
+ readonly target_text: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
44
+ readonly role: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
45
+ readonly evidence: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
46
+ readonly confirmed: v.BooleanSchema<undefined>;
47
+ readonly provenance: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
48
+ readonly created_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
49
+ readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
50
+ readonly author: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
51
+ }, undefined>, undefined>, v.MaxLengthAction<{
52
+ ref: string;
53
+ from: string;
54
+ to: string | null;
55
+ kind: "about" | "answered-by" | "assigned-to" | "blocked-by" | "created-from" | "duplicate-of" | "evidence" | "follow-up-of" | "labelled" | "links-to" | "member-of" | "related-to";
56
+ origin: "file" | "message" | "owner" | "suggested";
57
+ anchor: string | null;
58
+ target_text: string | null;
59
+ role: string | null;
60
+ evidence: string | null;
61
+ confirmed: boolean;
62
+ provenance: string | null;
63
+ created_at: number;
64
+ updated_at: number;
65
+ author: string | null;
66
+ }[], 1000, undefined>]>;
67
+ readonly external: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
68
+ readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
69
+ readonly content_sha256: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>;
70
+ }, undefined>, undefined>, v.MaxLengthAction<{
71
+ ref: string;
72
+ content_sha256: string | null;
73
+ }[], 1000, undefined>]>;
74
+ readonly version: v.LiteralSchema<2, undefined>;
75
+ readonly coverage: v.StrictObjectSchema<{
76
+ readonly aliases: v.LiteralSchema<"complete", undefined>;
77
+ readonly facts: v.LiteralSchema<"complete", undefined>;
78
+ readonly links: v.LiteralSchema<"complete", undefined>;
79
+ readonly memories: v.LiteralSchema<"omitted", undefined>;
80
+ readonly tasks: v.LiteralSchema<"omitted", undefined>;
81
+ readonly labels: v.LiteralSchema<"omitted", undefined>;
82
+ readonly findings: v.LiteralSchema<"omitted", undefined>;
83
+ readonly meeting_memberships: v.LiteralSchema<"omitted", undefined>;
84
+ readonly notes: v.LiteralSchema<"omitted", undefined>;
85
+ }, undefined>;
86
+ readonly project: v.StrictObjectSchema<{
87
+ readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
88
+ readonly key: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>;
89
+ readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
90
+ readonly type: v.PicklistSchema<["work", "client", "personal", "oss", "other"], undefined>;
91
+ readonly scope: v.PicklistSchema<readonly ["personal", "work"], undefined>;
92
+ readonly organization: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
93
+ readonly created_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
94
+ readonly description: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>, v.CheckAction<string, undefined>]>, undefined>;
95
+ readonly status: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>, v.CheckAction<string, undefined>]>, undefined>;
96
+ readonly owner: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>, v.CheckAction<string, undefined>]>, undefined>;
97
+ readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
98
+ readonly account_scope: v.LiteralSchema<"owner", undefined>;
99
+ }, undefined>;
100
+ readonly aliases: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
101
+ readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>, v.CheckAction<string, undefined>]>;
102
+ readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>, v.CheckAction<string, undefined>]>;
103
+ readonly display: v.BooleanSchema<undefined>;
104
+ readonly created_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
105
+ readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
106
+ }, undefined>, undefined>, v.MaxLengthAction<{
107
+ name: string;
108
+ source: string;
109
+ display: boolean;
110
+ created_at: number;
111
+ updated_at: number;
112
+ }[], 1000, undefined>]>;
113
+ }, undefined>;
114
+ export type ProjectAnnotationSnapshot = v.InferOutput<typeof schema>;
115
+ export declare const snapshotGraph: (value: ProjectAnnotationSnapshot) => ProjectAnnotations;
116
+ export declare const decodeProjectAnnotationSnapshot: (json: string) => ProjectAnnotationSnapshot;
117
+ export declare const projectAnnotationSnapshotFingerprint: (json: string) => string;
118
+ export {};
@@ -0,0 +1,92 @@
1
+ import { createHash } from "node:crypto";
2
+ import { CliError } from "@wirecat/cli-base";
3
+ import * as v from "valibot";
4
+ import { PROJECT_ANNOTATIONS_MAX_BYTES, validateProjectAnnotationsGraph } from "./manifest.js";
5
+ import { projectAnnotationsSchema } from "./schema.js";
6
+ const text = (max) => v.pipe(v.string(), v.minLength(1), v.maxLength(max), v.check((s) => !!s.trim()));
7
+ const timestamp = v.pipe(v.number(), v.safeInteger(), v.minValue(0));
8
+ const schema = v.strictObject({
9
+ ...projectAnnotationsSchema.entries,
10
+ version: v.literal(2),
11
+ coverage: v.strictObject({
12
+ ...projectAnnotationsSchema.entries.coverage.entries,
13
+ meeting_memberships: v.literal("omitted"),
14
+ notes: v.literal("omitted"),
15
+ }),
16
+ project: v.strictObject({
17
+ ...projectAnnotationsSchema.entries.project.entries,
18
+ description: v.nullable(text(20000)),
19
+ status: v.nullable(text(200)),
20
+ owner: v.nullable(text(2048)),
21
+ updated_at: timestamp,
22
+ account_scope: v.literal("owner"),
23
+ }),
24
+ aliases: v.pipe(v.array(v.strictObject({
25
+ name: text(200),
26
+ source: text(200),
27
+ display: v.boolean(),
28
+ created_at: timestamp,
29
+ updated_at: timestamp,
30
+ })), v.maxLength(1000)),
31
+ });
32
+ export const snapshotGraph = (value) => ({
33
+ format: value.format,
34
+ version: 1,
35
+ project: {
36
+ ref: value.project.ref,
37
+ key: value.project.key,
38
+ name: value.project.name,
39
+ type: value.project.type,
40
+ scope: value.project.scope,
41
+ organization: value.project.organization,
42
+ created_at: value.project.created_at,
43
+ },
44
+ coverage: value.coverage,
45
+ aliases: value.aliases.map((alias) => alias.name),
46
+ facts: value.facts,
47
+ links: value.links,
48
+ external: value.external,
49
+ });
50
+ export const decodeProjectAnnotationSnapshot = (json) => {
51
+ const invalid = (field) => {
52
+ throw new CliError("validation_error", `invalid project annotation snapshot.${field}`);
53
+ };
54
+ if (typeof json !== "string" || Buffer.byteLength(json) > PROJECT_ANNOTATIONS_MAX_BYTES)
55
+ return invalid("bytes");
56
+ let value;
57
+ try {
58
+ value = JSON.parse(json);
59
+ }
60
+ catch {
61
+ return invalid("json");
62
+ }
63
+ const checked = v.safeParse(schema, value);
64
+ if (!checked.success)
65
+ return invalid(checked.issues[0]?.path?.map((p) => String(p.key)).join(".") ?? "manifest");
66
+ const snapshot = checked.output;
67
+ if (snapshot.aliases.length + snapshot.facts.length + snapshot.links.length + snapshot.external.length > 1000)
68
+ return invalid("record budget");
69
+ if (snapshot.project.updated_at < snapshot.project.created_at)
70
+ return invalid("project.updated_at");
71
+ for (const alias of snapshot.aliases)
72
+ if (alias.updated_at < alias.created_at)
73
+ return invalid("aliases.updated_at");
74
+ for (const fact of snapshot.facts)
75
+ if (!fact.statement.trim())
76
+ return invalid("facts.statement");
77
+ validateProjectAnnotationsGraph(snapshotGraph(snapshot), snapshot.project.owner === null ? [] : [snapshot.project.owner]);
78
+ return snapshot;
79
+ };
80
+ export const projectAnnotationSnapshotFingerprint = (json) => {
81
+ const snapshot = decodeProjectAnnotationSnapshot(json);
82
+ const order = (a, b) => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0);
83
+ return createHash("sha256")
84
+ .update(JSON.stringify({
85
+ ...snapshot,
86
+ aliases: snapshot.aliases.toSorted((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)),
87
+ facts: snapshot.facts.toSorted(order),
88
+ links: snapshot.links.toSorted(order),
89
+ external: snapshot.external.toSorted(order),
90
+ }))
91
+ .digest("hex");
92
+ };
@@ -0,0 +1,17 @@
1
+ import { type ProjectAnnotationSnapshot } from "./snapshot-model.js";
2
+ export type { ProjectAnnotationSnapshot } from "./snapshot-model.js";
3
+ export { decodeProjectAnnotationSnapshot, projectAnnotationSnapshotFingerprint } from "./snapshot-model.js";
4
+ export interface ProjectAnnotationSnapshotOptions {
5
+ source: string;
6
+ project: string;
7
+ env?: NodeJS.ProcessEnv;
8
+ maxReadBytes?: number;
9
+ }
10
+ export interface ProjectAnnotationSnapshotResult {
11
+ snapshot: ProjectAnnotationSnapshot;
12
+ fingerprint: string;
13
+ read_bytes: number;
14
+ records: number;
15
+ }
16
+ /** No source maintenance, source fetching or output-file publication belongs to a snapshot read. */
17
+ export declare const snapshotProjectAnnotations: (options: ProjectAnnotationSnapshotOptions) => Promise<ProjectAnnotationSnapshotResult>;
@@ -0,0 +1,29 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { openStoreContext } from "../sqlite/open-store.js";
3
+ import { SnapshotBudget } from "../sqlite/project-transfer/budget.js";
4
+ import { projectAnnotationSnapshot } from "../sqlite/project-transfer/snapshot.js";
5
+ import { projectAnnotationSnapshotFingerprint } from "./snapshot-model.js";
6
+ export { decodeProjectAnnotationSnapshot, projectAnnotationSnapshotFingerprint } from "./snapshot-model.js";
7
+ /** No source maintenance, source fetching or output-file publication belongs to a snapshot read. */
8
+ export const snapshotProjectAnnotations = async (options) => {
9
+ if (!options || typeof options !== "object" || Array.isArray(options))
10
+ throw new CliError("validation_error", "snapshot options are required");
11
+ for (const field of Object.keys(options))
12
+ if (!["source", "project", "env", "maxReadBytes"].includes(field))
13
+ throw new CliError("validation_error", `unknown snapshot field ${field}`);
14
+ if (typeof options.source !== "string" || !options.source.trim() || options.source.length > 4096)
15
+ throw new CliError("validation_error", "snapshot source is an explicit existing store path");
16
+ if (typeof options.project !== "string" ||
17
+ !/^project:[1-9]\d*$/.test(options.project) ||
18
+ !Number.isSafeInteger(Number(options.project.slice(8))))
19
+ throw new CliError("validation_error", "snapshot project is a project:<safe integer id> reference");
20
+ const budget = new SnapshotBudget(options.maxReadBytes ?? 4 * 1024 * 1024);
21
+ const context = await openStoreContext({ path: options.source, env: options.env, readOnly: true });
22
+ try {
23
+ const result = projectAnnotationSnapshot(context.database, Number(options.project.slice(8)), budget);
24
+ return { ...result, fingerprint: projectAnnotationSnapshotFingerprint(JSON.stringify(result.snapshot)) };
25
+ }
26
+ finally {
27
+ context.database.close();
28
+ }
29
+ };
@@ -0,0 +1,9 @@
1
+ import type { CacheDatabase, SqlValue } from "../../driver.js";
2
+ export declare class SnapshotBudget {
3
+ readonly maxBytes: number;
4
+ records: number;
5
+ bytes: number;
6
+ constructor(maxBytes: number);
7
+ reserve(records: number, bytes: number): void;
8
+ rows(database: CacheDatabase, from: string, fields: string[], parameters: SqlValue[]): void;
9
+ }
@@ -0,0 +1,27 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ export class SnapshotBudget {
3
+ maxBytes;
4
+ records = 0;
5
+ bytes = 0;
6
+ constructor(maxBytes) {
7
+ this.maxBytes = maxBytes;
8
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 4 * 1024 * 1024)
9
+ throw new CliError("validation_error", "snapshot maxReadBytes takes 1–4194304 bytes");
10
+ }
11
+ reserve(records, bytes) {
12
+ if (!Number.isSafeInteger(records) ||
13
+ records < 0 ||
14
+ !Number.isSafeInteger(bytes) ||
15
+ bytes < 0 ||
16
+ this.records + records > 1000 ||
17
+ this.bytes + bytes > this.maxBytes)
18
+ throw new CliError("validation_error", "project annotation snapshot exceeds its record or input byte budget");
19
+ this.records += records;
20
+ this.bytes += bytes;
21
+ }
22
+ rows(database, from, fields, parameters) {
23
+ const bytes = fields.map((field) => `coalesce(length(CAST(${field} AS BLOB)),0)`).join("+");
24
+ const sized = database.prepare(`SELECT ${bytes} AS bytes ${from} LIMIT 1001`).all(...parameters);
25
+ this.reserve(sized.length, sized.reduce((sum, row) => sum + Number(row.bytes) + 128, 0));
26
+ }
27
+ }
@@ -0,0 +1,78 @@
1
+ import type { CacheDatabase } from "../../driver.js";
2
+ import type { SnapshotBudget } from "./budget.js";
3
+ export declare const projectAnnotationSnapshot: (database: CacheDatabase, projectId: number, budget: SnapshotBudget) => {
4
+ snapshot: {
5
+ format: "wirecat.project-annotations";
6
+ facts: {
7
+ ref: string;
8
+ type: "decision" | "fact" | "promise" | "question" | "statement";
9
+ statement: string;
10
+ status: "accepted" | "proposed" | "reversed" | "superseded";
11
+ subject: string | null;
12
+ occurred_at: number | null;
13
+ supersedes: string | null;
14
+ author: string;
15
+ model: string | null;
16
+ confirmed_by: string | null;
17
+ scope: "personal" | "work" | null;
18
+ source: string;
19
+ created_at: number;
20
+ updated_at: number;
21
+ }[];
22
+ links: {
23
+ ref: string;
24
+ from: string;
25
+ to: string | null;
26
+ kind: "about" | "answered-by" | "assigned-to" | "blocked-by" | "created-from" | "duplicate-of" | "evidence" | "follow-up-of" | "labelled" | "links-to" | "member-of" | "related-to";
27
+ origin: "file" | "message" | "owner" | "suggested";
28
+ anchor: string | null;
29
+ target_text: string | null;
30
+ role: string | null;
31
+ evidence: string | null;
32
+ confirmed: boolean;
33
+ provenance: string | null;
34
+ created_at: number;
35
+ updated_at: number;
36
+ author: string | null;
37
+ }[];
38
+ external: {
39
+ ref: string;
40
+ content_sha256: string | null;
41
+ }[];
42
+ version: 2;
43
+ coverage: {
44
+ aliases: "complete";
45
+ facts: "complete";
46
+ links: "complete";
47
+ memories: "omitted";
48
+ tasks: "omitted";
49
+ labels: "omitted";
50
+ findings: "omitted";
51
+ meeting_memberships: "omitted";
52
+ notes: "omitted";
53
+ };
54
+ project: {
55
+ ref: string;
56
+ key: string;
57
+ name: string;
58
+ type: "client" | "oss" | "other" | "personal" | "work";
59
+ scope: "personal" | "work";
60
+ organization: string | null;
61
+ created_at: number;
62
+ description: string | null;
63
+ status: string | null;
64
+ owner: string | null;
65
+ updated_at: number;
66
+ account_scope: "owner";
67
+ };
68
+ aliases: {
69
+ name: string;
70
+ source: string;
71
+ display: boolean;
72
+ created_at: number;
73
+ updated_at: number;
74
+ }[];
75
+ };
76
+ read_bytes: number;
77
+ records: number;
78
+ };
@@ -0,0 +1,183 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { MIGRATIONS } from "../../migrations.js";
3
+ import { decodeProjectAnnotationSnapshot } from "../../project-transfer/snapshot-model.js";
4
+ import { read } from "../transaction.js";
5
+ import { ProjectSnapshotSources } from "./sources.js";
6
+ const unsupported = () => {
7
+ throw new CliError("configuration_error", "project snapshot has unsupported metadata, deletion or account scope");
8
+ };
9
+ const nullableText = (value) => (value === null ? null : String(value));
10
+ const flag = (value) => {
11
+ if (value !== 0 && value !== 1)
12
+ return unsupported();
13
+ return value === 1;
14
+ };
15
+ const provenance = (raw) => {
16
+ if (raw === null)
17
+ return null;
18
+ let value;
19
+ try {
20
+ value = JSON.parse(String(raw));
21
+ }
22
+ catch {
23
+ return unsupported();
24
+ }
25
+ if (!value ||
26
+ typeof value !== "object" ||
27
+ Array.isArray(value) ||
28
+ Object.keys(value).some((key) => key !== "provenance"))
29
+ return unsupported();
30
+ const text = value.provenance;
31
+ if (text !== undefined && text !== null && typeof text !== "string")
32
+ return unsupported();
33
+ return text ?? null;
34
+ };
35
+ export const projectAnnotationSnapshot = (database, projectId, budget) => read(database, () => {
36
+ if (database.prepare("SELECT max(version) AS version FROM schema_migrations").get()?.version !==
37
+ MIGRATIONS.at(-1)?.version)
38
+ throw new CliError("configuration_error", "project snapshots require the SDK's current store schema without source migration");
39
+ const sources = new ProjectSnapshotSources(database, budget);
40
+ const rows = (from, fields, params) => {
41
+ budget.rows(database, from, fields, params);
42
+ return database.prepare(`SELECT ${fields.join(",")} ${from} LIMIT 1001`).all(...params);
43
+ };
44
+ const descriptor = database.prepare("SELECT account_id,deleted_at FROM projects WHERE id=?").get(projectId);
45
+ if (!descriptor)
46
+ throw new CliError("not_found", "the source project does not exist");
47
+ if (descriptor.account_id !== null || descriptor.deleted_at !== null)
48
+ return unsupported();
49
+ const project = rows("FROM projects WHERE id=?", [
50
+ "id",
51
+ "key",
52
+ "name",
53
+ "description",
54
+ "type",
55
+ "scope",
56
+ "organization_id",
57
+ "owner_type",
58
+ "owner_id",
59
+ "status",
60
+ "created_at",
61
+ "updated_at",
62
+ ], [projectId])[0];
63
+ if (!project)
64
+ throw new CliError("not_found", "the source project does not exist");
65
+ const aliases = rows("FROM aliases WHERE aliasable_type='project' AND aliasable_id=? ORDER BY id", ["name", "source", "display", "account_id", "created_at", "updated_at"], [projectId]);
66
+ if (aliases.some((alias) => alias.account_id !== null))
67
+ return unsupported();
68
+ if (database
69
+ .prepare("SELECT 1 FROM facts WHERE project_id=? AND (metadata IS NOT NULL OR deleted_at IS NOT NULL) LIMIT 1")
70
+ .get(projectId))
71
+ return unsupported();
72
+ const facts = rows("FROM facts WHERE project_id=? ORDER BY id", [
73
+ "id",
74
+ "type",
75
+ "statement",
76
+ "status",
77
+ "subject_type",
78
+ "subject_id",
79
+ "occurred_at",
80
+ "supersedes_id",
81
+ "author_type",
82
+ "author_id",
83
+ "model",
84
+ "confirmed_by_type",
85
+ "confirmed_by_id",
86
+ "scope",
87
+ "source",
88
+ "created_at",
89
+ "updated_at",
90
+ ], [projectId]);
91
+ const links = rows("FROM links WHERE (from_type='project' AND from_id=?) OR (to_type='project' AND to_id=?) OR " +
92
+ "(from_type='fact' AND from_id IN (SELECT id FROM facts WHERE project_id=?)) OR " +
93
+ "(to_type='fact' AND to_id IN (SELECT id FROM facts WHERE project_id=?)) ORDER BY id", [
94
+ "id",
95
+ "from_type",
96
+ "from_id",
97
+ "to_type",
98
+ "to_id",
99
+ "kind",
100
+ "anchor",
101
+ "source",
102
+ "target_text",
103
+ "role",
104
+ "evidence",
105
+ "metadata",
106
+ "confirmed",
107
+ "author",
108
+ "created_at",
109
+ "updated_at",
110
+ ], [projectId, projectId, projectId, projectId]);
111
+ const projectRef = `project:${projectId}`;
112
+ const owned = new Set([projectRef, ...facts.map((row) => `fact:${row.id}`)]);
113
+ const data = {
114
+ format: "wirecat.project-annotations",
115
+ version: 2,
116
+ project: {
117
+ ref: projectRef,
118
+ key: project.key,
119
+ name: project.name,
120
+ type: project.type,
121
+ scope: project.scope,
122
+ organization: project.organization_id === null ? null : sources.ref("organization", project.organization_id),
123
+ created_at: project.created_at,
124
+ description: project.description,
125
+ status: project.status,
126
+ owner: sources.ref(project.owner_type, project.owner_id),
127
+ updated_at: project.updated_at,
128
+ account_scope: "owner",
129
+ },
130
+ coverage: {
131
+ aliases: "complete",
132
+ facts: "complete",
133
+ links: "complete",
134
+ memories: "omitted",
135
+ tasks: "omitted",
136
+ labels: "omitted",
137
+ findings: "omitted",
138
+ meeting_memberships: "omitted",
139
+ notes: "omitted",
140
+ },
141
+ aliases: aliases.map((row) => ({
142
+ name: row.name,
143
+ source: row.source,
144
+ display: flag(row.display),
145
+ created_at: row.created_at,
146
+ updated_at: row.updated_at,
147
+ })),
148
+ facts: facts.map((row) => ({
149
+ ref: `fact:${row.id}`,
150
+ type: row.type,
151
+ statement: row.statement,
152
+ status: row.status,
153
+ subject: sources.ref(row.subject_type, row.subject_id),
154
+ occurred_at: row.occurred_at,
155
+ supersedes: row.supersedes_id === null ? null : sources.ref("fact", row.supersedes_id),
156
+ author: sources.ref(row.author_type, row.author_id),
157
+ model: row.model,
158
+ confirmed_by: sources.ref(row.confirmed_by_type, row.confirmed_by_id),
159
+ scope: row.scope,
160
+ source: row.source,
161
+ created_at: row.created_at,
162
+ updated_at: row.updated_at,
163
+ })),
164
+ links: links.map((row) => ({
165
+ ref: `link:${row.id}`,
166
+ from: sources.ref(row.from_type, row.from_id),
167
+ to: sources.ref(row.to_type, row.to_id),
168
+ kind: row.kind,
169
+ origin: row.source,
170
+ anchor: nullableText(row.anchor),
171
+ target_text: nullableText(row.target_text),
172
+ role: nullableText(row.role),
173
+ evidence: nullableText(row.evidence),
174
+ confirmed: flag(row.confirmed),
175
+ provenance: provenance(row.metadata),
176
+ created_at: row.created_at,
177
+ updated_at: row.updated_at,
178
+ author: nullableText(row.author),
179
+ })),
180
+ };
181
+ const snapshot = decodeProjectAnnotationSnapshot(JSON.stringify({ ...data, external: sources.external(owned) }));
182
+ return { snapshot, read_bytes: budget.bytes, records: budget.records };
183
+ });
@@ -0,0 +1,18 @@
1
+ import type { CacheDatabase } from "../../driver.js";
2
+ import { type Thing } from "../things.js";
3
+ import type { SnapshotBudget } from "./budget.js";
4
+ export declare class ProjectSnapshotSources {
5
+ private readonly database;
6
+ private readonly budget;
7
+ readonly references: Map<string, Thing>;
8
+ private readonly byThing;
9
+ constructor(database: CacheDatabase, budget: SnapshotBudget);
10
+ ref(type: unknown, id: unknown): string | null;
11
+ external(owned: Set<string>): {
12
+ ref: string;
13
+ content_sha256: string | null;
14
+ }[];
15
+ private fingerprint;
16
+ private transcript;
17
+ private digest;
18
+ }
@@ -0,0 +1,148 @@
1
+ import { createHash } from "node:crypto";
2
+ import { CliError } from "@wirecat/cli-base";
3
+ import { referenceOfThing, storedThing } from "../things.js";
4
+ const invalid = () => {
5
+ throw new CliError("configuration_error", "project snapshot has an unavailable or unsupported dependency");
6
+ };
7
+ const scalar = new Set([
8
+ "person",
9
+ "bot",
10
+ "organization",
11
+ "project",
12
+ "fact",
13
+ "memory",
14
+ "task",
15
+ "note",
16
+ "document",
17
+ "message",
18
+ "email",
19
+ "chat",
20
+ "identity",
21
+ "account",
22
+ "meeting",
23
+ "meeting_transcript",
24
+ "meeting_transcript_row",
25
+ ]);
26
+ const noText = new Set(["person", "bot", "organization", "project", "chat", "identity", "account"]);
27
+ const texts = {
28
+ document: {
29
+ table: "documents",
30
+ fields: ["n.title", "n.body"],
31
+ where: "n.deleted_at IS NULL AND n.body IS NOT NULL AND CASE WHEN json_valid(n.metadata) THEN coalesce(json_extract(n.metadata,'$.catalog.active'),1) ELSE 1 END=1",
32
+ },
33
+ note: { table: "notes", fields: ["n.title", "n.body"], where: "n.deleted_at IS NULL" },
34
+ message: {
35
+ table: "messages",
36
+ fields: ["n.text", "n.sent_at", "n.sender_name", "n.sender_chat_external_id", "i.provider", "i.external_id"],
37
+ join: "LEFT JOIN identities i ON i.id=n.sender_identity_id",
38
+ where: "n.deleted_at IS NULL",
39
+ },
40
+ email: {
41
+ table: "emails",
42
+ fields: ["n.subject", "coalesce(n.body_text,'')", "n.sent_at", "n.from_address", "n.from_name"],
43
+ where: "n.deleted_at IS NULL",
44
+ },
45
+ meeting: {
46
+ table: "meetings",
47
+ fields: ["n.title", "n.description", "n.location", "n.started_at", "n.ended_at", "n.duration_ms", "n.timezone"],
48
+ where: "n.deleted_at IS NULL",
49
+ },
50
+ meeting_transcript_row: {
51
+ table: "meeting_transcript_rows",
52
+ fields: ["n.position", "n.start_ms", "n.end_ms", "n.speaker_name", "n.text"],
53
+ join: "JOIN meeting_transcripts t ON t.id=n.meeting_transcript_id JOIN meetings m ON m.id=t.meeting_id",
54
+ where: "t.deleted_at IS NULL AND m.deleted_at IS NULL",
55
+ },
56
+ };
57
+ export class ProjectSnapshotSources {
58
+ database;
59
+ budget;
60
+ references = new Map();
61
+ byThing = new Map();
62
+ constructor(database, budget) {
63
+ this.database = database;
64
+ this.budget = budget;
65
+ }
66
+ ref(type, id) {
67
+ if (type === null && id === null)
68
+ return null;
69
+ if (typeof type !== "string" || !scalar.has(type) || typeof id !== "number" || !Number.isSafeInteger(id) || id < 1)
70
+ return invalid();
71
+ const key = `${type}:${id}`, cached = this.byThing.get(key);
72
+ if (cached)
73
+ return cached;
74
+ if (type === "project" || type === "organization") {
75
+ const table = type === "project" ? "projects" : "organizations";
76
+ if (!this.database.prepare(`SELECT 1 FROM ${table} WHERE id=? AND deleted_at IS NULL`).get(id))
77
+ return invalid();
78
+ }
79
+ const identifier = {
80
+ message: [
81
+ "FROM messages n JOIN chats c ON c.id=n.chat_id JOIN accounts a ON a.id=n.account_id WHERE n.id=?",
82
+ ["n.external_id", "c.external_id", "a.external_id", "a.provider"],
83
+ ],
84
+ email: [
85
+ "FROM emails n JOIN email_threads c ON c.id=n.email_thread_id JOIN accounts a ON a.id=n.account_id WHERE n.id=?",
86
+ ["n.external_id", "c.external_id", "a.external_id", "a.provider"],
87
+ ],
88
+ chat: [
89
+ "FROM chats n JOIN accounts a ON a.id=n.account_id WHERE n.id=?",
90
+ ["n.external_id", "a.external_id", "a.provider"],
91
+ ],
92
+ identity: ["FROM identities n WHERE n.id=?", ["n.external_id", "n.provider"]],
93
+ };
94
+ const selection = identifier[type];
95
+ if (selection)
96
+ this.budget.rows(this.database, selection[0], [...selection[1]], [id]);
97
+ const thing = storedThing(type, id), ref = referenceOfThing(this.database, thing);
98
+ if (!ref)
99
+ return invalid();
100
+ this.byThing.set(key, ref);
101
+ this.references.set(ref, thing);
102
+ return ref;
103
+ }
104
+ external(owned) {
105
+ return [...this.references]
106
+ .filter(([ref]) => !owned.has(ref))
107
+ .map(([ref, thing]) => ({
108
+ ref,
109
+ content_sha256: this.fingerprint(thing),
110
+ }));
111
+ }
112
+ fingerprint(thing) {
113
+ this.budget.reserve(1, 128);
114
+ if (noText.has(thing.type))
115
+ return null;
116
+ if (thing.type === "meeting_transcript")
117
+ return this.transcript(thing.id);
118
+ const spec = texts[thing.type];
119
+ if (!spec)
120
+ return invalid();
121
+ const from = `FROM ${spec.table} n ${spec.join ?? ""} WHERE n.id=? AND ${spec.where}`;
122
+ this.budget.rows(this.database, from, spec.fields, [thing.id]);
123
+ const row = this.database
124
+ .prepare(`SELECT ${spec.fields.map((f, i) => `${f} AS v${i}`).join(",")} ${from}`)
125
+ .get(thing.id);
126
+ if (!row)
127
+ return invalid();
128
+ return this.digest(thing.type, spec.fields.map((_f, i) => row[`v${i}`]));
129
+ }
130
+ transcript(id) {
131
+ const from = "FROM meeting_transcripts n JOIN meetings m ON m.id=n.meeting_id WHERE n.id=? AND n.deleted_at IS NULL AND m.deleted_at IS NULL";
132
+ this.budget.rows(this.database, from, ["n.format", "n.language", "n.source"], [id]);
133
+ const header = this.database.prepare(`SELECT n.format,n.language,n.source ${from}`).get(id);
134
+ if (!header)
135
+ return invalid();
136
+ const rows = "FROM meeting_transcript_rows n WHERE n.meeting_transcript_id=?";
137
+ this.budget.rows(this.database, rows, ["n.position", "n.start_ms", "n.end_ms", "n.speaker_name", "n.text"], [id]);
138
+ const cues = this.database
139
+ .prepare(`SELECT n.position,n.start_ms,n.end_ms,n.speaker_name,n.text ${rows} ORDER BY n.position LIMIT 1001`)
140
+ .all(id);
141
+ return this.digest("meeting_transcript", [header, cues]);
142
+ }
143
+ digest(kind, content) {
144
+ return createHash("sha256")
145
+ .update(JSON.stringify(["wirecat.project-source", 1, kind, content]))
146
+ .digest("hex");
147
+ }
148
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.372.0",
3
+ "version": "0.373.0",
4
4
  "description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Viacheslav Ptsarev",