@wirecat/cli-core 0.373.0 → 0.375.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.
Files changed (31) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/files/bounded-utf8.d.ts +4 -0
  3. package/dist/files/bounded-utf8.js +32 -0
  4. package/dist/files/index.d.ts +1 -0
  5. package/dist/files/index.js +1 -0
  6. package/dist/search/lucene/registry.js +1 -1
  7. package/dist/store/index.d.ts +4 -1
  8. package/dist/store/index.js +3 -1
  9. package/dist/store/project-transfer/plan.d.ts +17 -0
  10. package/dist/store/project-transfer/plan.js +18 -7
  11. package/dist/store/project-transfer/preview.d.ts +11 -0
  12. package/dist/store/project-transfer/preview.js +29 -0
  13. package/dist/store/project-transfer/request.d.ts +76 -0
  14. package/dist/store/project-transfer/request.js +22 -0
  15. package/dist/store/project-transfer/snapshot-model.d.ts +7 -8
  16. package/dist/store/project-transfer/snapshot-model.js +10 -3
  17. package/dist/store/project-transfer/snapshot-plan.d.ts +12 -0
  18. package/dist/store/project-transfer/snapshot-plan.js +4 -0
  19. package/dist/store/project-transfer/snapshot.d.ts +1 -1
  20. package/dist/store/project-transfer/snapshot.js +1 -1
  21. package/dist/store/project-transfer/store.d.ts +25 -0
  22. package/dist/store/project-transfer/store.js +1 -0
  23. package/dist/store/sqlite/project-transfer/preview.d.ts +12 -0
  24. package/dist/store/sqlite/project-transfer/preview.js +74 -0
  25. package/dist/store/sqlite/project-transfer/records.d.ts +5 -0
  26. package/dist/store/sqlite/project-transfer/records.js +53 -0
  27. package/dist/store/sqlite/project-transfer/store.d.ts +3 -0
  28. package/dist/store/sqlite/project-transfer/store.js +35 -0
  29. package/dist/store/store.d.ts +2 -0
  30. package/dist/store/store.js +5 -0
  31. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -7,6 +7,35 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.375.0 — 20.10.2026
11
+
12
+ ### Fixed
13
+
14
+ - **A WhatsApp chat id works unquoted in a query.** `chat:133…@lid` and `chat:120…@g.us` parse as one term; before,
15
+ `@lid` was refused with `query.unsupported-operator`. The fix is in cli-base 0.47.3, so cli-core now needs
16
+ `@wirecat/cli-base >=0.47.3`: a CLI pinned to 0.47.0–0.47.2 must raise its own cli-base pin with this release.
17
+ Emails in the `email` field help and the search docs are unquoted too.
18
+
19
+ ## 0.374.0 — 20.10.2026
20
+
21
+ ### Added
22
+
23
+ - **`readBoundedUtf8File` from `./files`** preflights ordinary-file size, bounds reads even if the input grows,
24
+ and refuses invalid UTF-8. Snapshot hosts can share this reader and `projectAnnotationSnapshotSchema`
25
+ from `./store` for transport validation; graph validation still uses the snapshot decoder.
26
+ - **The optional `store.projectAnnotations` port** previews and atomically restores selected version-2
27
+ annotations into an empty selected target. Restore requires the exact preview fingerprint, rechecks
28
+ references and namespace under the write lock, preserves historical state and rolls back on failure.
29
+ It never opens or migrates another store. Hosts must enforce owner authorization; no CLI/MCP mutation
30
+ host or live reset is included, and omitted facets remain outside this selected format.
31
+ - **`previewProjectAnnotationSnapshot` from `./store`** reads an explicit existing destination natively
32
+ read-only and checks version-2 bindings against its actual content hashes, selected annotations and
33
+ project vocabulary. It reports descriptor/namespace conflicts and a preview fingerprint in one coherent
34
+ bounded read. It neither restores annotations nor authorizes a write.
35
+ - **`planProjectAnnotationSnapshot` from `./store`** validates explicit bindings for version-2 snapshots,
36
+ including the project's owner, and binds preserved alias/project metadata to the preview fingerprint.
37
+ Version-1 planning keeps its existing contract. This pure helper opens no store and does not apply records.
38
+
10
39
  ## 0.373.0 — 20.10.2026
11
40
 
12
41
  ### Added
@@ -0,0 +1,4 @@
1
+ /** Preflight size, bound growing input too, and refuse replacement characters in structured input. */
2
+ export declare const readBoundedUtf8File: (path: string, { maxBytes }: {
3
+ maxBytes: number;
4
+ }) => Promise<string>;
@@ -0,0 +1,32 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { openOrdinaryFile } from "./ordinary-file.js";
3
+ /** Preflight size, bound growing input too, and refuse replacement characters in structured input. */
4
+ export const readBoundedUtf8File = async (path, { maxBytes }) => {
5
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 16 * 1024 * 1024)
6
+ throw new CliError("validation_error", "file maxBytes takes 1–16777216 bytes");
7
+ const file = await openOrdinaryFile(path);
8
+ try {
9
+ if ((await file.stat()).size > maxBytes)
10
+ throw new CliError("validation_error", "input file exceeds its byte budget");
11
+ const chunks = [], buffer = Buffer.alloc(Math.min(65536, maxBytes + 1));
12
+ let bytes = 0;
13
+ for (;;) {
14
+ const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, maxBytes - bytes + 1), null);
15
+ if (bytesRead === 0)
16
+ break;
17
+ bytes += bytesRead;
18
+ if (bytes > maxBytes)
19
+ throw new CliError("validation_error", "input file exceeds its byte budget");
20
+ chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
21
+ }
22
+ try {
23
+ return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, bytes));
24
+ }
25
+ catch {
26
+ throw new CliError("validation_error", "input file must contain valid UTF-8");
27
+ }
28
+ }
29
+ finally {
30
+ await file.close();
31
+ }
32
+ };
@@ -1,4 +1,5 @@
1
1
  export { type AtomicWriteOptions, writeFileAtomic } from "./atomic-write.js";
2
+ export { readBoundedUtf8File } from "./bounded-utf8.js";
2
3
  export { acquireLock, type FileLock } from "./lock.js";
3
4
  export type { NativeWriterLeaseLayout } from "./native-writer-lease.js";
4
5
  export { nativeWriterLeaseStore } from "./native-writer-lease.js";
@@ -1,4 +1,5 @@
1
1
  export { writeFileAtomic } from "./atomic-write.js";
2
+ export { readBoundedUtf8File } from "./bounded-utf8.js";
2
3
  export { acquireLock } from "./lock.js";
3
4
  export { nativeWriterLeaseStore } from "./native-writer-lease.js";
4
5
  export { openOrdinaryFile, sameOrdinaryFile, syncDirectory } from "./ordinary-file.js";
@@ -355,7 +355,7 @@ export const QUERY_FIELDS = [
355
355
  'phone:"8 900 000-00-01"',
356
356
  "a phone found in the text, written any way; a number without + is read in phone_countries",
357
357
  ],
358
- ["email", 'email:"ana@example.com"', "an address found in the text, lower-cased; quoted, since @ is an operator"],
358
+ ["email", "email:ana@example.com", "an address found in the text, lower-cased"],
359
359
  ["url", 'url:"https://example.com/a"', "a link found in the text, without utm_*, fbclid and the fragment"],
360
360
  ["domain", "domain:google.com", "the site of a link or an address in the text; docs.google.com means google.com"],
361
361
  [
@@ -16,7 +16,10 @@ 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
+ export { type ProjectAnnotationPreview, type ProjectAnnotationPreviewOptions, previewProjectAnnotationSnapshot, } from "./project-transfer/preview.js";
20
+ export { decodeProjectAnnotationSnapshot, type ProjectAnnotationSnapshot, type ProjectAnnotationSnapshotOptions, type ProjectAnnotationSnapshotResult, projectAnnotationSnapshotFingerprint, projectAnnotationSnapshotSchema, snapshotProjectAnnotations, } from "./project-transfer/snapshot.js";
21
+ export { planProjectAnnotationSnapshot } from "./project-transfer/snapshot-plan.js";
22
+ export type { ProjectAnnotationRestoreReceipt, ProjectAnnotationRestoreRequest, ProjectAnnotationStore, ProjectAnnotationTargetRequest, } from "./project-transfer/store.js";
20
23
  export type { AgentAction, AgentActionInput, AgentActionsStore } from "./sqlite/agent-actions.js";
21
24
  export { resetAttachmentWords } from "./sqlite/attachment-texts.js";
22
25
  export { backfillNormalized, pendingNormalization } from "./sqlite/backfill.js";
@@ -8,7 +8,9 @@ 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
+ export { previewProjectAnnotationSnapshot, } from "./project-transfer/preview.js";
12
+ export { decodeProjectAnnotationSnapshot, projectAnnotationSnapshotFingerprint, projectAnnotationSnapshotSchema, snapshotProjectAnnotations, } from "./project-transfer/snapshot.js";
13
+ export { planProjectAnnotationSnapshot } from "./project-transfer/snapshot-plan.js";
12
14
  export { resetAttachmentWords } from "./sqlite/attachment-texts.js";
13
15
  export { backfillNormalized, pendingNormalization } from "./sqlite/backfill.js";
14
16
  export { CATALOG_QUERY } from "./sqlite/catalog.js";
@@ -23,6 +23,23 @@ export interface ProjectAnnotationsPlan {
23
23
  }[];
24
24
  ready: boolean;
25
25
  }
26
+ export declare const projectAnnotationBindingSources: (manifest: {
27
+ project: {
28
+ ref: string;
29
+ };
30
+ external: {
31
+ ref: string;
32
+ }[];
33
+ }, bindings: Record<string, string>) => Set<string>;
34
+ export declare const planBoundProjectAnnotations: (manifest: {
35
+ project: {
36
+ ref: string;
37
+ };
38
+ external: {
39
+ ref: string;
40
+ content_sha256: string | null;
41
+ }[];
42
+ }, manifest_fingerprint: string, target: ProjectAnnotationsTarget, bindings: Record<string, string>) => ProjectAnnotationsPlan;
26
43
  /** Pure planning over an adapter's complete target snapshot; a ready plan is not write authorization. */
27
44
  export declare const planProjectAnnotations: (json: string, target: ProjectAnnotationsTarget, bindings: Record<string, string>) => ProjectAnnotationsPlan;
28
45
  export {};
@@ -26,12 +26,7 @@ const kind = (ref) => {
26
26
  const invalid = (field) => {
27
27
  throw new CliError("validation_error", `invalid project annotation plan.${field}`);
28
28
  };
29
- /** Pure planning over an adapter's complete target snapshot; a ready plan is not write authorization. */
30
- export const planProjectAnnotations = (json, target, bindings) => {
31
- const manifest = decodeProjectAnnotations(json);
32
- const checked = v.safeParse(targetSchema, target);
33
- if (!checked.success)
34
- return invalid(checked.issues[0]?.path?.map((p) => String(p.key)).join(".") ?? "target");
29
+ export const projectAnnotationBindingSources = (manifest, bindings) => {
35
30
  if (!bindings || typeof bindings !== "object" || Array.isArray(bindings) || Object.keys(bindings).length > 1001)
36
31
  return invalid("bindings");
37
32
  if (Object.getPrototypeOf(bindings) !== Object.prototype && Object.getPrototypeOf(bindings) !== null)
@@ -40,6 +35,21 @@ export const planProjectAnnotations = (json, target, bindings) => {
40
35
  for (const [source, destination] of Object.entries(bindings))
41
36
  if (!required.has(source) || typeof destination !== "string" || destination.length > 2048)
42
37
  return invalid("bindings");
38
+ for (const destination of Object.values(bindings)) {
39
+ try {
40
+ kind(destination);
41
+ }
42
+ catch {
43
+ return invalid("bindings.reference");
44
+ }
45
+ }
46
+ return required;
47
+ };
48
+ export const planBoundProjectAnnotations = (manifest, manifest_fingerprint, target, bindings) => {
49
+ const checked = v.safeParse(targetSchema, target);
50
+ if (!checked.success)
51
+ return invalid(checked.issues[0]?.path?.map((p) => String(p.key)).join(".") ?? "target");
52
+ const required = projectAnnotationBindingSources(manifest, bindings);
43
53
  const destinations = new Map();
44
54
  for (const item of checked.output.external) {
45
55
  try {
@@ -84,7 +94,6 @@ export const planProjectAnnotations = (json, target, bindings) => {
84
94
  }
85
95
  if (target.annotation_count > 0)
86
96
  conflicts.push({ reference: target.project, reason: "populated" });
87
- const manifest_fingerprint = projectAnnotationsFingerprint(json);
88
97
  const fingerprint = createHash("sha256")
89
98
  .update(JSON.stringify({
90
99
  manifest_fingerprint,
@@ -104,3 +113,5 @@ export const planProjectAnnotations = (json, target, bindings) => {
104
113
  ready: missing_bindings.length === 0 && conflicts.length === 0,
105
114
  };
106
115
  };
116
+ /** Pure planning over an adapter's complete target snapshot; a ready plan is not write authorization. */
117
+ export const planProjectAnnotations = (json, target, bindings) => planBoundProjectAnnotations(decodeProjectAnnotations(json), projectAnnotationsFingerprint(json), target, bindings);
@@ -0,0 +1,11 @@
1
+ import type { ProjectAnnotationPreview } from "./snapshot-plan.js";
2
+ export type { ProjectAnnotationPreview } from "./snapshot-plan.js";
3
+ export interface ProjectAnnotationPreviewOptions {
4
+ snapshot: string;
5
+ target: string;
6
+ project: string;
7
+ bindings: Record<string, string>;
8
+ maxReadBytes?: number;
9
+ env?: NodeJS.ProcessEnv;
10
+ }
11
+ export declare const previewProjectAnnotationSnapshot: (options: ProjectAnnotationPreviewOptions) => Promise<ProjectAnnotationPreview>;
@@ -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 { previewAnnotationSnapshot } from "../sqlite/project-transfer/preview.js";
5
+ import { projectAnnotationBindingSources } from "./plan.js";
6
+ import { decodeProjectAnnotationSnapshot } from "./snapshot-model.js";
7
+ export const previewProjectAnnotationSnapshot = async (options) => {
8
+ if (!options || typeof options !== "object" || Array.isArray(options))
9
+ throw new CliError("validation_error", "project preview options are required");
10
+ for (const field of Object.keys(options))
11
+ if (!["snapshot", "target", "project", "bindings", "maxReadBytes", "env"].includes(field))
12
+ throw new CliError("validation_error", `unknown project preview field ${field}`);
13
+ const snapshot = decodeProjectAnnotationSnapshot(options.snapshot);
14
+ projectAnnotationBindingSources(snapshot, options.bindings);
15
+ if (typeof options.target !== "string" || !options.target.trim() || options.target.length > 4096)
16
+ throw new CliError("validation_error", "project preview target is an explicit existing store path");
17
+ if (typeof options.project !== "string" ||
18
+ !/^project:[1-9]\d*$/.test(options.project) ||
19
+ !Number.isSafeInteger(Number(options.project.slice(8))))
20
+ throw new CliError("validation_error", "project preview project is a project:<safe integer id> reference");
21
+ const budget = new SnapshotBudget(options.maxReadBytes ?? 4 * 1024 * 1024);
22
+ const context = await openStoreContext({ path: options.target, env: options.env, readOnly: true });
23
+ try {
24
+ return previewAnnotationSnapshot(context.database, Number(options.project.slice(8)), snapshot, options.bindings, budget);
25
+ }
26
+ finally {
27
+ context.database.close();
28
+ }
29
+ };
@@ -0,0 +1,76 @@
1
+ import type { ProjectAnnotationTargetRequest } from "./store.js";
2
+ export declare const projectAnnotationRequest: (request: ProjectAnnotationTargetRequest, restore?: boolean) => {
3
+ snapshot: {
4
+ format: "wirecat.project-annotations";
5
+ facts: {
6
+ ref: string;
7
+ type: "decision" | "fact" | "promise" | "question" | "statement";
8
+ statement: string;
9
+ status: "accepted" | "proposed" | "reversed" | "superseded";
10
+ subject: string | null;
11
+ occurred_at: number | null;
12
+ supersedes: string | null;
13
+ author: string;
14
+ model: string | null;
15
+ confirmed_by: string | null;
16
+ scope: "personal" | "work" | null;
17
+ source: string;
18
+ created_at: number;
19
+ updated_at: number;
20
+ }[];
21
+ links: {
22
+ ref: string;
23
+ from: string;
24
+ to: string | null;
25
+ kind: "about" | "answered-by" | "assigned-to" | "blocked-by" | "created-from" | "duplicate-of" | "evidence" | "follow-up-of" | "labelled" | "links-to" | "member-of" | "related-to";
26
+ origin: "file" | "message" | "owner" | "suggested";
27
+ anchor: string | null;
28
+ target_text: string | null;
29
+ role: string | null;
30
+ evidence: string | null;
31
+ confirmed: boolean;
32
+ provenance: string | null;
33
+ created_at: number;
34
+ updated_at: number;
35
+ author: string | null;
36
+ }[];
37
+ external: {
38
+ ref: string;
39
+ content_sha256: string | null;
40
+ }[];
41
+ version: 2;
42
+ coverage: {
43
+ aliases: "complete";
44
+ facts: "complete";
45
+ links: "complete";
46
+ memories: "omitted";
47
+ tasks: "omitted";
48
+ labels: "omitted";
49
+ findings: "omitted";
50
+ meeting_memberships: "omitted";
51
+ notes: "omitted";
52
+ };
53
+ project: {
54
+ ref: string;
55
+ key: string;
56
+ name: string;
57
+ type: "client" | "oss" | "other" | "personal" | "work";
58
+ scope: "personal" | "work";
59
+ organization: string | null;
60
+ created_at: number;
61
+ description: string | null;
62
+ status: string | null;
63
+ owner: string | null;
64
+ updated_at: number;
65
+ account_scope: "owner";
66
+ };
67
+ aliases: {
68
+ name: string;
69
+ source: string;
70
+ display: boolean;
71
+ created_at: number;
72
+ updated_at: number;
73
+ }[];
74
+ };
75
+ projectId: number;
76
+ };
@@ -0,0 +1,22 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { projectAnnotationBindingSources } from "./plan.js";
3
+ import { decodeProjectAnnotationSnapshot } from "./snapshot-model.js";
4
+ export const projectAnnotationRequest = (request, restore = false) => {
5
+ if (!request || typeof request !== "object" || Array.isArray(request))
6
+ throw new CliError("validation_error", "project annotation request is required");
7
+ const fields = ["snapshot", "project", "bindings", "maxReadBytes", ...(restore ? ["fingerprint"] : [])];
8
+ for (const field of Object.keys(request))
9
+ if (!fields.includes(field))
10
+ throw new CliError("validation_error", `unknown project annotation field ${field}`);
11
+ const snapshot = decodeProjectAnnotationSnapshot(request.snapshot);
12
+ projectAnnotationBindingSources(snapshot, request.bindings);
13
+ if (typeof request.project !== "string" ||
14
+ !/^project:[1-9]\d*$/.test(request.project) ||
15
+ !Number.isSafeInteger(Number(request.project.slice(8))))
16
+ throw new CliError("validation_error", "project annotation project is a project:<safe integer id> reference");
17
+ if (restore &&
18
+ (typeof request.fingerprint !== "string" ||
19
+ !/^[a-f0-9]{64}$/.test(request.fingerprint)))
20
+ throw new CliError("validation_error", "project annotation fingerprint is required");
21
+ return { snapshot, projectId: Number(request.project.slice(8)) };
22
+ };
@@ -1,6 +1,6 @@
1
1
  import * as v from "valibot";
2
2
  import { type ProjectAnnotations } from "./schema.js";
3
- declare const schema: v.StrictObjectSchema<{
3
+ export declare const projectAnnotationSnapshotSchema: v.StrictObjectSchema<{
4
4
  readonly format: v.LiteralSchema<"wirecat.project-annotations", undefined>;
5
5
  readonly facts: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
6
6
  readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
@@ -91,15 +91,15 @@ declare const schema: v.StrictObjectSchema<{
91
91
  readonly scope: v.PicklistSchema<readonly ["personal", "work"], undefined>;
92
92
  readonly organization: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
93
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>;
94
+ readonly description: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
95
+ readonly status: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
96
+ readonly owner: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
97
97
  readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
98
98
  readonly account_scope: v.LiteralSchema<"owner", undefined>;
99
99
  }, undefined>;
100
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>]>;
101
+ readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
102
+ readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
103
103
  readonly display: v.BooleanSchema<undefined>;
104
104
  readonly created_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
105
105
  readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
@@ -111,8 +111,7 @@ declare const schema: v.StrictObjectSchema<{
111
111
  updated_at: number;
112
112
  }[], 1000, undefined>]>;
113
113
  }, undefined>;
114
- export type ProjectAnnotationSnapshot = v.InferOutput<typeof schema>;
114
+ export type ProjectAnnotationSnapshot = v.InferOutput<typeof projectAnnotationSnapshotSchema>;
115
115
  export declare const snapshotGraph: (value: ProjectAnnotationSnapshot) => ProjectAnnotations;
116
116
  export declare const decodeProjectAnnotationSnapshot: (json: string) => ProjectAnnotationSnapshot;
117
117
  export declare const projectAnnotationSnapshotFingerprint: (json: string) => string;
118
- export {};
@@ -3,9 +3,9 @@ import { CliError } from "@wirecat/cli-base";
3
3
  import * as v from "valibot";
4
4
  import { PROJECT_ANNOTATIONS_MAX_BYTES, validateProjectAnnotationsGraph } from "./manifest.js";
5
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()));
6
+ const text = (max) => v.pipe(v.string(), v.minLength(1), v.maxLength(max));
7
7
  const timestamp = v.pipe(v.number(), v.safeInteger(), v.minValue(0));
8
- const schema = v.strictObject({
8
+ export const projectAnnotationSnapshotSchema = v.strictObject({
9
9
  ...projectAnnotationsSchema.entries,
10
10
  version: v.literal(2),
11
11
  coverage: v.strictObject({
@@ -60,12 +60,19 @@ export const decodeProjectAnnotationSnapshot = (json) => {
60
60
  catch {
61
61
  return invalid("json");
62
62
  }
63
- const checked = v.safeParse(schema, value);
63
+ const checked = v.safeParse(projectAnnotationSnapshotSchema, value);
64
64
  if (!checked.success)
65
65
  return invalid(checked.issues[0]?.path?.map((p) => String(p.key)).join(".") ?? "manifest");
66
66
  const snapshot = checked.output;
67
67
  if (snapshot.aliases.length + snapshot.facts.length + snapshot.links.length + snapshot.external.length > 1000)
68
68
  return invalid("record budget");
69
+ for (const field of ["description", "status", "owner"])
70
+ if (snapshot.project[field] !== null && !snapshot.project[field].trim())
71
+ return invalid(`project.${field}`);
72
+ for (const alias of snapshot.aliases)
73
+ for (const field of ["name", "source"])
74
+ if (!alias[field].trim())
75
+ return invalid(`aliases.${field}`);
69
76
  if (snapshot.project.updated_at < snapshot.project.created_at)
70
77
  return invalid("project.updated_at");
71
78
  for (const alias of snapshot.aliases)
@@ -0,0 +1,12 @@
1
+ import { type ProjectAnnotationsPlan, type ProjectAnnotationsTarget } from "./plan.js";
2
+ export interface ProjectAnnotationPreview {
3
+ plan: ProjectAnnotationsPlan;
4
+ descriptor_conflicts: ("key" | "name")[];
5
+ namespace_conflicts: string[];
6
+ ready: boolean;
7
+ fingerprint: string;
8
+ read_bytes: number;
9
+ records: number;
10
+ }
11
+ /** Metadata and project-owner bindings participate in the version-2 preview fingerprint. */
12
+ export declare const planProjectAnnotationSnapshot: (json: string, target: ProjectAnnotationsTarget, bindings: Record<string, string>) => ProjectAnnotationsPlan;
@@ -0,0 +1,4 @@
1
+ import { planBoundProjectAnnotations } from "./plan.js";
2
+ import { decodeProjectAnnotationSnapshot, projectAnnotationSnapshotFingerprint } from "./snapshot-model.js";
3
+ /** Metadata and project-owner bindings participate in the version-2 preview fingerprint. */
4
+ export const planProjectAnnotationSnapshot = (json, target, bindings) => planBoundProjectAnnotations(decodeProjectAnnotationSnapshot(json), projectAnnotationSnapshotFingerprint(json), target, bindings);
@@ -1,6 +1,6 @@
1
1
  import { type ProjectAnnotationSnapshot } from "./snapshot-model.js";
2
2
  export type { ProjectAnnotationSnapshot } from "./snapshot-model.js";
3
- export { decodeProjectAnnotationSnapshot, projectAnnotationSnapshotFingerprint } from "./snapshot-model.js";
3
+ export { decodeProjectAnnotationSnapshot, projectAnnotationSnapshotFingerprint, projectAnnotationSnapshotSchema, } from "./snapshot-model.js";
4
4
  export interface ProjectAnnotationSnapshotOptions {
5
5
  source: string;
6
6
  project: string;
@@ -3,7 +3,7 @@ import { openStoreContext } from "../sqlite/open-store.js";
3
3
  import { SnapshotBudget } from "../sqlite/project-transfer/budget.js";
4
4
  import { projectAnnotationSnapshot } from "../sqlite/project-transfer/snapshot.js";
5
5
  import { projectAnnotationSnapshotFingerprint } from "./snapshot-model.js";
6
- export { decodeProjectAnnotationSnapshot, projectAnnotationSnapshotFingerprint } from "./snapshot-model.js";
6
+ export { decodeProjectAnnotationSnapshot, projectAnnotationSnapshotFingerprint, projectAnnotationSnapshotSchema, } from "./snapshot-model.js";
7
7
  /** No source maintenance, source fetching or output-file publication belongs to a snapshot read. */
8
8
  export const snapshotProjectAnnotations = async (options) => {
9
9
  if (!options || typeof options !== "object" || Array.isArray(options))
@@ -0,0 +1,25 @@
1
+ import type { ProjectAnnotationPreview } from "./snapshot-plan.js";
2
+ export interface ProjectAnnotationTargetRequest {
3
+ snapshot: string;
4
+ project: string;
5
+ bindings: Record<string, string>;
6
+ maxReadBytes?: number;
7
+ }
8
+ export interface ProjectAnnotationRestoreRequest extends ProjectAnnotationTargetRequest {
9
+ fingerprint: string;
10
+ }
11
+ export interface ProjectAnnotationRestoreReceipt {
12
+ project: string;
13
+ fingerprint: string;
14
+ mappings: Record<string, string>;
15
+ counts: {
16
+ aliases: number;
17
+ facts: number;
18
+ links: number;
19
+ };
20
+ }
21
+ /** Hosts authorize restore before calling; historical accepted state is not a new agent decision. */
22
+ export interface ProjectAnnotationStore {
23
+ preview(request: ProjectAnnotationTargetRequest): Promise<ProjectAnnotationPreview>;
24
+ restore(request: ProjectAnnotationRestoreRequest): Promise<ProjectAnnotationRestoreReceipt>;
25
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { CacheDatabase } from "../../driver.js";
2
+ import { type ProjectAnnotationSnapshot } from "../../project-transfer/snapshot-model.js";
3
+ import type { SnapshotBudget } from "./budget.js";
4
+ export declare const previewAnnotationSnapshot: (database: CacheDatabase, projectId: number, manifest: ProjectAnnotationSnapshot, bindings: Record<string, string>, budget: SnapshotBudget) => {
5
+ plan: import("../../index.js").ProjectAnnotationsPlan;
6
+ descriptor_conflicts: ("key" | "name")[];
7
+ namespace_conflicts: string[];
8
+ ready: boolean;
9
+ fingerprint: string;
10
+ read_bytes: number;
11
+ records: number;
12
+ };
@@ -0,0 +1,74 @@
1
+ import { createHash } from "node:crypto";
2
+ import { normalizeVocabularyName } from "../../../domain/vocabulary.js";
3
+ import { projectAnnotationSnapshotFingerprint, } from "../../project-transfer/snapshot-model.js";
4
+ import { planProjectAnnotationSnapshot } from "../../project-transfer/snapshot-plan.js";
5
+ import { thingOf } from "../things.js";
6
+ import { read } from "../transaction.js";
7
+ import { projectAnnotationSnapshot } from "./snapshot.js";
8
+ import { ProjectSnapshotSources } from "./sources.js";
9
+ export const previewAnnotationSnapshot = (database, projectId, manifest, bindings, budget) => read(database, () => {
10
+ const current = projectAnnotationSnapshot(database, projectId, budget).snapshot;
11
+ const sources = new ProjectSnapshotSources(database, budget);
12
+ for (const ref of new Set(Object.values(bindings))) {
13
+ if (ref === current.project.ref)
14
+ continue;
15
+ const thing = thingOf(database, ref);
16
+ if (thing)
17
+ sources.ref(thing.type, thing.id);
18
+ }
19
+ const external = sources.external(new Set([current.project.ref]));
20
+ const projectFields = ["id", "key", "name", "account_id", "deleted_at"];
21
+ const aliasFields = ["aliasable_id", "name", "account_id"];
22
+ const projectFrom = "FROM projects ORDER BY id";
23
+ const aliasFrom = "FROM aliases WHERE aliasable_type='project' ORDER BY id";
24
+ budget.rows(database, projectFrom, projectFields, []);
25
+ budget.rows(database, aliasFrom, aliasFields, []);
26
+ const projects = database.prepare(`SELECT ${projectFields.join(",")} ${projectFrom} LIMIT 1001`).all();
27
+ const aliases = database.prepare(`SELECT ${aliasFields.join(",")} ${aliasFrom} LIMIT 1001`).all();
28
+ const names = new Set([manifest.project.key, manifest.project.name, ...manifest.aliases.map((a) => a.name)].map(normalizeVocabularyName));
29
+ const namespace = new Set();
30
+ for (const project of projects) {
31
+ if (project.id === projectId)
32
+ continue;
33
+ const keyMatches = names.has(normalizeVocabularyName(String(project.key)));
34
+ const nameMatches = project.account_id === null &&
35
+ project.deleted_at === null &&
36
+ names.has(normalizeVocabularyName(String(project.name)));
37
+ if (keyMatches || nameMatches)
38
+ namespace.add(`project:${project.id}`);
39
+ }
40
+ for (const alias of aliases) {
41
+ if (alias.aliasable_id === projectId || alias.account_id !== null)
42
+ continue;
43
+ const project = projects.find((p) => p.id === alias.aliasable_id && p.account_id === null && p.deleted_at === null);
44
+ if (project && names.has(normalizeVocabularyName(String(alias.name))))
45
+ namespace.add(`project:${project.id}`);
46
+ }
47
+ const descriptor_conflicts = ["key", "name"].filter((field) => current.project[field] !== manifest.project[field]);
48
+ const target = {
49
+ project: current.project.ref,
50
+ annotation_count: current.aliases.length + current.facts.length + current.links.length,
51
+ revision_sha256: createHash("sha256")
52
+ .update(JSON.stringify({
53
+ snapshot: projectAnnotationSnapshotFingerprint(JSON.stringify(current)),
54
+ projects,
55
+ aliases,
56
+ external: external.toSorted((a, b) => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0)),
57
+ }))
58
+ .digest("hex"),
59
+ external,
60
+ };
61
+ const plan = planProjectAnnotationSnapshot(JSON.stringify(manifest), target, bindings);
62
+ const namespace_conflicts = [...namespace].sort();
63
+ return {
64
+ plan,
65
+ descriptor_conflicts,
66
+ namespace_conflicts,
67
+ ready: plan.ready && descriptor_conflicts.length === 0 && namespace_conflicts.length === 0,
68
+ fingerprint: createHash("sha256")
69
+ .update(JSON.stringify({ plan: plan.fingerprint, descriptor_conflicts, namespace_conflicts }))
70
+ .digest("hex"),
71
+ read_bytes: budget.bytes,
72
+ records: budget.records,
73
+ };
74
+ });
@@ -0,0 +1,5 @@
1
+ import type { CacheDatabase } from "../../driver.js";
2
+ import type { ProjectAnnotationSnapshot } from "../../project-transfer/snapshot-model.js";
3
+ export declare const restoreAnnotationRecords: (database: CacheDatabase, projectId: number, snapshot: ProjectAnnotationSnapshot, bindings: Record<string, string>) => {
4
+ [x: string]: string;
5
+ };
@@ -0,0 +1,53 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { fold } from "../../normalize.js";
3
+ import { thingOf } from "../things.js";
4
+ export const restoreAnnotationRecords = (database, projectId, snapshot, bindings) => {
5
+ const mappings = { ...bindings };
6
+ const resolve = (ref) => {
7
+ if (ref === null)
8
+ return null;
9
+ const mapped = mappings[ref], thing = mapped === undefined ? undefined : thingOf(database, mapped);
10
+ if (!thing)
11
+ throw new CliError("validation_error", "project annotation binding became unavailable");
12
+ return thing;
13
+ };
14
+ const organization = resolve(snapshot.project.organization), owner = resolve(snapshot.project.owner);
15
+ database
16
+ .prepare("UPDATE projects SET description=?,type=?,scope=?,organization_id=?,owner_type=?,owner_id=?,status=?,created_at=?,updated_at=? WHERE id=?")
17
+ .run(snapshot.project.description, snapshot.project.type, snapshot.project.scope, organization?.id ?? null, owner?.type ?? null, owner?.id ?? null, snapshot.project.status, snapshot.project.created_at, snapshot.project.updated_at, projectId);
18
+ for (const alias of snapshot.aliases)
19
+ database
20
+ .prepare("INSERT INTO aliases(aliasable_type,aliasable_id,account_id,name,name_folded,display,source,created_at,updated_at) VALUES('project',?,NULL,?,?,?,?,?,?)")
21
+ .run(projectId, alias.name, fold(alias.name), Number(alias.display), alias.source, alias.created_at, alias.updated_at);
22
+ for (const fact of snapshot.facts) {
23
+ const author = resolve(fact.author), confirmed = resolve(fact.confirmed_by);
24
+ if (!author)
25
+ throw new CliError("validation_error", "project annotation author is required");
26
+ const row = database
27
+ .prepare("INSERT INTO facts(type,statement,status,project_id,subject_type,subject_id,occurred_at,supersedes_id,author_type,author_id,model,confirmed_by_type,confirmed_by_id,scope,source,created_at,updated_at) VALUES(?,?,?,?,NULL,NULL,?,NULL,?,?,?,?,?,?,?,?,?) RETURNING id")
28
+ .get(fact.type, fact.statement, fact.status, projectId, fact.occurred_at, author.type, author.id, fact.model, confirmed?.type ?? null, confirmed?.id ?? null, fact.scope, fact.source, fact.created_at, fact.updated_at);
29
+ if (!row)
30
+ throw new CliError("configuration_error", "project annotation fact insertion failed");
31
+ mappings[fact.ref] = `fact:${row.id}`;
32
+ }
33
+ for (const fact of snapshot.facts) {
34
+ const subject = resolve(fact.subject), previous = resolve(fact.supersedes), target = resolve(fact.ref);
35
+ if (!target)
36
+ throw new CliError("configuration_error", "project annotation fact mapping is missing");
37
+ database
38
+ .prepare("UPDATE facts SET subject_type=?,subject_id=?,supersedes_id=? WHERE id=?")
39
+ .run(subject?.type ?? null, subject?.id ?? null, previous?.id ?? null, target.id);
40
+ }
41
+ for (const link of snapshot.links) {
42
+ const from = resolve(link.from), to = resolve(link.to);
43
+ if (!from)
44
+ throw new CliError("validation_error", "project annotation link source is required");
45
+ const row = database
46
+ .prepare("INSERT INTO links(from_type,from_id,to_type,to_id,kind,anchor,source,target_text,target_folded,role,evidence,metadata,confirmed,created_at,author,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) RETURNING id")
47
+ .get(from.type, from.id, to?.type ?? null, to?.id ?? null, link.kind, link.anchor, link.origin, link.target_text, link.target_text === null ? null : fold(link.target_text), link.role, link.evidence, link.provenance === null ? null : JSON.stringify({ provenance: link.provenance }), Number(link.confirmed), link.created_at, link.author, link.updated_at);
48
+ if (!row)
49
+ throw new CliError("configuration_error", "project annotation link insertion failed");
50
+ mappings[link.ref] = `link:${row.id}`;
51
+ }
52
+ return mappings;
53
+ };
@@ -0,0 +1,3 @@
1
+ import type { ProjectAnnotationStore } from "../../project-transfer/store.js";
2
+ import type { StoreContext } from "../open.js";
3
+ export declare const projectAnnotationStoreOver: ({ database }: StoreContext) => ProjectAnnotationStore;
@@ -0,0 +1,35 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { projectAnnotationRequest } from "../../project-transfer/request.js";
3
+ import { write } from "../transaction.js";
4
+ import { SnapshotBudget } from "./budget.js";
5
+ import { previewAnnotationSnapshot } from "./preview.js";
6
+ import { restoreAnnotationRecords } from "./records.js";
7
+ export const projectAnnotationStoreOver = ({ database }) => ({
8
+ async preview(request) {
9
+ const { snapshot, projectId } = projectAnnotationRequest(request);
10
+ return previewAnnotationSnapshot(database, projectId, snapshot, request.bindings, new SnapshotBudget(request.maxReadBytes ?? 4 * 1024 * 1024));
11
+ },
12
+ async restore(request) {
13
+ const { snapshot, projectId } = projectAnnotationRequest(request, true);
14
+ const budget = new SnapshotBudget(request.maxReadBytes ?? 4 * 1024 * 1024);
15
+ try {
16
+ return write(database, () => {
17
+ const preview = previewAnnotationSnapshot(database, projectId, snapshot, request.bindings, budget);
18
+ if (!preview.ready || preview.fingerprint !== request.fingerprint)
19
+ throw new CliError("validation_error", "project annotation preview is stale or has conflicts; preview again");
20
+ const mappings = restoreAnnotationRecords(database, projectId, snapshot, request.bindings);
21
+ return {
22
+ project: request.project,
23
+ fingerprint: preview.fingerprint,
24
+ mappings,
25
+ counts: { aliases: snapshot.aliases.length, facts: snapshot.facts.length, links: snapshot.links.length },
26
+ };
27
+ });
28
+ }
29
+ catch (error) {
30
+ if (error instanceof CliError)
31
+ throw error;
32
+ throw new CliError("configuration_error", "project annotation restore failed; its transaction was rolled back");
33
+ }
34
+ },
35
+ });
@@ -10,6 +10,7 @@ import type { MeetingFindingsStore } from "./meeting-findings.js";
10
10
  import { type MentionsStore } from "./mentions.js";
11
11
  import { type MessagesStore } from "./messages.js";
12
12
  import { type PeopleStore } from "./people.js";
13
+ import type { ProjectAnnotationStore } from "./project-transfer/store.js";
13
14
  import { type QueriesStore } from "./queries.js";
14
15
  import { type RosterStore } from "./roster.js";
15
16
  import { type SearchIndexStore } from "./search-index.js";
@@ -34,6 +35,7 @@ import { type TagsStore } from "./tags.js";
34
35
  import type { AccountKey } from "./types.js";
35
36
  import { type VectorsStore } from "./vectors.js";
36
37
  export interface MessageStore {
38
+ readonly projectAnnotations?: ProjectAnnotationStore;
37
39
  readonly contextEvidence?: ContextEvidenceReader;
38
40
  readonly contextIndex?: ContextIndex;
39
41
  readonly contextRecords?: ContextRecordReader;
@@ -31,6 +31,7 @@ import { sourceMentionStoreOver } from "./sqlite/mention-sources.js";
31
31
  import { noteSearchOver } from "./sqlite/note-search.js";
32
32
  import { notesStoreOver } from "./sqlite/notes.js";
33
33
  import { openStoreContext } from "./sqlite/open-store.js";
34
+ import { projectAnnotationStoreOver } from "./sqlite/project-transfer/store.js";
34
35
  import { proposedActionsStoreOver } from "./sqlite/proposed-actions.js";
35
36
  import * as stems from "./sqlite/stems.js";
36
37
  import { taskStoreOver } from "./sqlite/tasks.js";
@@ -82,6 +83,7 @@ const storeOver = (context) => {
82
83
  mentions: areaOf(() => accountMentionsOver(context), (keyed) => mentionsStoreOver(context, keyed)),
83
84
  };
84
85
  const contextEvidence = once(() => contextEvidenceOver(context));
86
+ const projectAnnotations = once(() => projectAnnotationStoreOver(context));
85
87
  const contextIndex = once(() => contextIndexOver(context, stemmerFor));
86
88
  const contextRecords = once(() => contextRecordsOver(context));
87
89
  const searches = once(() => searchesStoreOver(context));
@@ -102,6 +104,9 @@ const storeOver = (context) => {
102
104
  const agentActions = once(() => agentActionsStoreOver(context, pruneLogsWhenDue));
103
105
  const meetings = once(() => meetingStoreOver(context));
104
106
  const store = {
107
+ get projectAnnotations() {
108
+ return projectAnnotations();
109
+ },
105
110
  get contextEvidence() {
106
111
  return contextEvidence();
107
112
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.373.0",
3
+ "version": "0.375.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",
@@ -202,7 +202,7 @@
202
202
  "@types/node": "^22.20.4",
203
203
  "@types/pngjs": "6.0.5",
204
204
  "@vitest/coverage-v8": "^5.0.2",
205
- "@wirecat/cli-base": "0.47.0",
205
+ "@wirecat/cli-base": "0.47.3",
206
206
  "commander": "15.0.0",
207
207
  "cspell": "10.3.6",
208
208
  "drizzle-kit": "1.0.0-rc.4",
@@ -222,7 +222,7 @@
222
222
  },
223
223
  "peerDependencies": {
224
224
  "@napi-rs/canvas": ">=0.1.69 <2",
225
- "@wirecat/cli-base": ">=0.47.0 <0.48.0",
225
+ "@wirecat/cli-base": ">=0.47.3 <0.48.0",
226
226
  "commander": ">=15"
227
227
  },
228
228
  "bin": {