@wirecat/cli-core 0.372.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 (38) hide show
  1. package/CHANGELOG.md +31 -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 -0
  7. package/dist/store/index.js +3 -0
  8. package/dist/store/project-transfer/manifest.d.ts +1 -0
  9. package/dist/store/project-transfer/manifest.js +6 -2
  10. package/dist/store/project-transfer/plan.d.ts +17 -0
  11. package/dist/store/project-transfer/plan.js +18 -7
  12. package/dist/store/project-transfer/preview.d.ts +11 -0
  13. package/dist/store/project-transfer/preview.js +29 -0
  14. package/dist/store/project-transfer/request.d.ts +76 -0
  15. package/dist/store/project-transfer/request.js +22 -0
  16. package/dist/store/project-transfer/snapshot-model.d.ts +117 -0
  17. package/dist/store/project-transfer/snapshot-model.js +99 -0
  18. package/dist/store/project-transfer/snapshot-plan.d.ts +12 -0
  19. package/dist/store/project-transfer/snapshot-plan.js +4 -0
  20. package/dist/store/project-transfer/snapshot.d.ts +17 -0
  21. package/dist/store/project-transfer/snapshot.js +29 -0
  22. package/dist/store/project-transfer/store.d.ts +25 -0
  23. package/dist/store/project-transfer/store.js +1 -0
  24. package/dist/store/sqlite/project-transfer/budget.d.ts +9 -0
  25. package/dist/store/sqlite/project-transfer/budget.js +27 -0
  26. package/dist/store/sqlite/project-transfer/preview.d.ts +12 -0
  27. package/dist/store/sqlite/project-transfer/preview.js +74 -0
  28. package/dist/store/sqlite/project-transfer/records.d.ts +5 -0
  29. package/dist/store/sqlite/project-transfer/records.js +53 -0
  30. package/dist/store/sqlite/project-transfer/snapshot.d.ts +78 -0
  31. package/dist/store/sqlite/project-transfer/snapshot.js +183 -0
  32. package/dist/store/sqlite/project-transfer/sources.d.ts +18 -0
  33. package/dist/store/sqlite/project-transfer/sources.js +148 -0
  34. package/dist/store/sqlite/project-transfer/store.d.ts +3 -0
  35. package/dist/store/sqlite/project-transfer/store.js +35 -0
  36. package/dist/store/store.d.ts +2 -0
  37. package/dist/store/store.js +5 -0
  38. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,37 @@ 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
+
30
+ ## 0.373.0 — 20.10.2026
31
+
32
+ ### Added
33
+
34
+ - **Read-only project annotation snapshots from `./store`** preserve project metadata, alias provenance
35
+ and timestamps, facts and direct links in a version-2 manifest. `snapshotProjectAnnotations` reads one
36
+ explicit project from an existing current-schema store in a consistent SQLite transaction, with
37
+ bounded source hashing and no migration or source text in the result. The decoder and fingerprint
38
+ helpers validate that format independently. Notes, meeting memberships, tasks, memories, labels
39
+ and findings remain explicitly omitted; restore and CLI commands are not included.
40
+
10
41
  ## 0.372.0 — 20.10.2026
11
42
 
12
43
  ### 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,6 +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 { 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";
19
23
  export type { AgentAction, AgentActionInput, AgentActionsStore } from "./sqlite/agent-actions.js";
20
24
  export { resetAttachmentWords } from "./sqlite/attachment-texts.js";
21
25
  export { backfillNormalized, pendingNormalization } from "./sqlite/backfill.js";
@@ -8,6 +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 { 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";
11
14
  export { resetAttachmentWords } from "./sqlite/attachment-texts.js";
12
15
  export { backfillNormalized, pendingNormalization } from "./sqlite/backfill.js";
13
16
  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) => {
@@ -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
+ };
@@ -0,0 +1,117 @@
1
+ import * as v from "valibot";
2
+ import { type ProjectAnnotations } from "./schema.js";
3
+ export declare const projectAnnotationSnapshotSchema: 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>]>, 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
+ 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>]>;
102
+ readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, 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 projectAnnotationSnapshotSchema>;
115
+ export declare const snapshotGraph: (value: ProjectAnnotationSnapshot) => ProjectAnnotations;
116
+ export declare const decodeProjectAnnotationSnapshot: (json: string) => ProjectAnnotationSnapshot;
117
+ export declare const projectAnnotationSnapshotFingerprint: (json: string) => string;
@@ -0,0 +1,99 @@
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));
7
+ const timestamp = v.pipe(v.number(), v.safeInteger(), v.minValue(0));
8
+ export const projectAnnotationSnapshotSchema = 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(projectAnnotationSnapshotSchema, 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
+ 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}`);
76
+ if (snapshot.project.updated_at < snapshot.project.created_at)
77
+ return invalid("project.updated_at");
78
+ for (const alias of snapshot.aliases)
79
+ if (alias.updated_at < alias.created_at)
80
+ return invalid("aliases.updated_at");
81
+ for (const fact of snapshot.facts)
82
+ if (!fact.statement.trim())
83
+ return invalid("facts.statement");
84
+ validateProjectAnnotationsGraph(snapshotGraph(snapshot), snapshot.project.owner === null ? [] : [snapshot.project.owner]);
85
+ return snapshot;
86
+ };
87
+ export const projectAnnotationSnapshotFingerprint = (json) => {
88
+ const snapshot = decodeProjectAnnotationSnapshot(json);
89
+ const order = (a, b) => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0);
90
+ return createHash("sha256")
91
+ .update(JSON.stringify({
92
+ ...snapshot,
93
+ aliases: snapshot.aliases.toSorted((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)),
94
+ facts: snapshot.facts.toSorted(order),
95
+ links: snapshot.links.toSorted(order),
96
+ external: snapshot.external.toSorted(order),
97
+ }))
98
+ .digest("hex");
99
+ };
@@ -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);
@@ -0,0 +1,17 @@
1
+ import { type ProjectAnnotationSnapshot } from "./snapshot-model.js";
2
+ export type { ProjectAnnotationSnapshot } from "./snapshot-model.js";
3
+ export { decodeProjectAnnotationSnapshot, projectAnnotationSnapshotFingerprint, projectAnnotationSnapshotSchema, } 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>;