@wirecat/cli-core 0.373.0 → 0.374.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 (30) hide show
  1. package/CHANGELOG.md +20 -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/store/index.d.ts +4 -1
  7. package/dist/store/index.js +3 -1
  8. package/dist/store/project-transfer/plan.d.ts +17 -0
  9. package/dist/store/project-transfer/plan.js +18 -7
  10. package/dist/store/project-transfer/preview.d.ts +11 -0
  11. package/dist/store/project-transfer/preview.js +29 -0
  12. package/dist/store/project-transfer/request.d.ts +76 -0
  13. package/dist/store/project-transfer/request.js +22 -0
  14. package/dist/store/project-transfer/snapshot-model.d.ts +7 -8
  15. package/dist/store/project-transfer/snapshot-model.js +10 -3
  16. package/dist/store/project-transfer/snapshot-plan.d.ts +12 -0
  17. package/dist/store/project-transfer/snapshot-plan.js +4 -0
  18. package/dist/store/project-transfer/snapshot.d.ts +1 -1
  19. package/dist/store/project-transfer/snapshot.js +1 -1
  20. package/dist/store/project-transfer/store.d.ts +25 -0
  21. package/dist/store/project-transfer/store.js +1 -0
  22. package/dist/store/sqlite/project-transfer/preview.d.ts +12 -0
  23. package/dist/store/sqlite/project-transfer/preview.js +74 -0
  24. package/dist/store/sqlite/project-transfer/records.d.ts +5 -0
  25. package/dist/store/sqlite/project-transfer/records.js +53 -0
  26. package/dist/store/sqlite/project-transfer/store.d.ts +3 -0
  27. package/dist/store/sqlite/project-transfer/store.js +35 -0
  28. package/dist/store/store.d.ts +2 -0
  29. package/dist/store/store.js +5 -0
  30. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.374.0 — 20.10.2026
11
+
12
+ ### Added
13
+
14
+ - **`readBoundedUtf8File` from `./files`** preflights ordinary-file size, bounds reads even if the input grows,
15
+ and refuses invalid UTF-8. Snapshot hosts can share this reader and `projectAnnotationSnapshotSchema`
16
+ from `./store` for transport validation; graph validation still uses the snapshot decoder.
17
+ - **The optional `store.projectAnnotations` port** previews and atomically restores selected version-2
18
+ annotations into an empty selected target. Restore requires the exact preview fingerprint, rechecks
19
+ references and namespace under the write lock, preserves historical state and rolls back on failure.
20
+ It never opens or migrates another store. Hosts must enforce owner authorization; no CLI/MCP mutation
21
+ host or live reset is included, and omitted facets remain outside this selected format.
22
+ - **`previewProjectAnnotationSnapshot` from `./store`** reads an explicit existing destination natively
23
+ read-only and checks version-2 bindings against its actual content hashes, selected annotations and
24
+ project vocabulary. It reports descriptor/namespace conflicts and a preview fingerprint in one coherent
25
+ bounded read. It neither restores annotations nor authorizes a write.
26
+ - **`planProjectAnnotationSnapshot` from `./store`** validates explicit bindings for version-2 snapshots,
27
+ including the project's owner, and binds preserved alias/project metadata to the preview fingerprint.
28
+ Version-1 planning keeps its existing contract. This pure helper opens no store and does not apply records.
29
+
10
30
  ## 0.373.0 — 20.10.2026
11
31
 
12
32
  ### 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";
@@ -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.374.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",