@wirecat/cli-core 0.371.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/setup-screen.d.ts +25 -0
- package/dist/cli/setup-screen.js +39 -0
- package/dist/cli/upgrade-command.d.ts +9 -0
- package/dist/cli/upgrade-command.js +26 -0
- package/dist/kit/argv-log.d.ts +4 -0
- package/dist/kit/argv-log.js +9 -0
- package/dist/kit/index.d.ts +1 -0
- package/dist/kit/index.js +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.js +1 -0
- package/dist/store/project-transfer/manifest.d.ts +1 -0
- package/dist/store/project-transfer/manifest.js +6 -2
- package/dist/store/project-transfer/snapshot-model.d.ts +118 -0
- package/dist/store/project-transfer/snapshot-model.js +92 -0
- package/dist/store/project-transfer/snapshot.d.ts +17 -0
- package/dist/store/project-transfer/snapshot.js +29 -0
- package/dist/store/sqlite/project-transfer/budget.d.ts +9 -0
- package/dist/store/sqlite/project-transfer/budget.js +27 -0
- package/dist/store/sqlite/project-transfer/snapshot.d.ts +78 -0
- package/dist/store/sqlite/project-transfer/snapshot.js +183 -0
- package/dist/store/sqlite/project-transfer/sources.d.ts +18 -0
- package/dist/store/sqlite/project-transfer/sources.js +148 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,42 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.373.0 — 20.10.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Read-only project annotation snapshots from `./store`** preserve project metadata, alias provenance
|
|
15
|
+
and timestamps, facts and direct links in a version-2 manifest. `snapshotProjectAnnotations` reads one
|
|
16
|
+
explicit project from an existing current-schema store in a consistent SQLite transaction, with
|
|
17
|
+
bounded source hashing and no migration or source text in the result. The decoder and fingerprint
|
|
18
|
+
helpers validate that format independently. Notes, meeting memberships, tasks, memories, labels
|
|
19
|
+
and findings remain explicitly omitted; restore and CLI commands are not included.
|
|
20
|
+
|
|
21
|
+
## 0.372.0 — 20.10.2026
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **Setup wizard helpers from `./cli`**: `AGENTS`, `type Agent`, `screenFor(context, quiet, steps)` and
|
|
26
|
+
`agentFor(context, given, pad, prompt?)`, for the `setup` command of a messenger CLI. `screenFor` prints the
|
|
27
|
+
numbered step headings (`[2/5] …`, details indented by 6) to stderr for a person, hides them under `--quiet`,
|
|
28
|
+
and keeps plain notes in a machine mode. `agentFor` asks which agent to install only at a terminal in the pretty
|
|
29
|
+
format and answers `none` otherwise; an unknown answer is a `validation_error`. Both take the minimum shape they
|
|
30
|
+
read (`format`, `streams.diagnostic`, `renderer.note`, `stdin`), so tg-cli's and whatsapp-cli's contexts fit as they
|
|
31
|
+
are. Pass the wizard's step count as `steps`, and the prompt's `input`, `output` and `signal` as `prompt`
|
|
32
|
+
(`secretInputOf(command)` plus the signal, or `{ input: context.stdin }`). No profile or agent is chosen for the
|
|
33
|
+
caller. This replaces the copies in both CLIs' `src/commands/setup.ts`.
|
|
34
|
+
- **`restartServers(app, script, env, spawn?)` from `./cli`** restarts the `serve` of every profile that is running,
|
|
35
|
+
with `<script> <profile> server restart`, and answers `{ restarted, left }`: the shape `upgradeCommand`'s
|
|
36
|
+
`afterUpdate` port returns, so `afterUpdate: () => restartServers(app, script, env, environment.spawn)` is the whole
|
|
37
|
+
host side. `script` is the new install's own entry point; `spawn` replaces the child process in a test. A serve started
|
|
38
|
+
by hand refuses the restart and lands in `left`. This replaces the copies in both CLIs' `src/commands/update.ts` and
|
|
39
|
+
`src/update.ts`.
|
|
40
|
+
- **`ARGV_LOG` and `argvLogSetup` from `./testing`**: the path `coverage/argv.jsonl` under the working directory, and
|
|
41
|
+
a vitest `globalSetup` function that clears it, for the test matrix that checks which commands and options a run
|
|
42
|
+
drove. A CLI's own `src/testing/argv-log.ts` can go. Vitest takes a package specifier as `globalSetup` only with a
|
|
43
|
+
named `setup` or a default export, so keep a one-line file: `export { argvLogSetup as default } from
|
|
44
|
+
"@wirecat/cli-core/testing"`.
|
|
45
|
+
|
|
10
46
|
## 0.371.0 — 20.10.2026
|
|
11
47
|
|
|
12
48
|
### Added
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -127,7 +127,8 @@ export { createProgram, type ProgramDefinition, type ProgramOptions, type RunOpt
|
|
|
127
127
|
export { fieldsOf, projectFields } from "./result-fields.js";
|
|
128
128
|
export { runsCommand } from "./runs/command.js";
|
|
129
129
|
export { type Config, type Configuration, count, first, flag, fromFile, type GlobalFlags, NONE_NAMED, type ProfileKind, plain, type ResolveOptions, type Settings, type SettingsExtension, type SettingsExtensionContext, type Source, settingsFor, } from "./settings.js";
|
|
130
|
+
export { AGENTS, type Agent, agentFor, screenFor } from "./setup-screen.js";
|
|
130
131
|
export { skillCommand } from "./skill-command.js";
|
|
131
132
|
export { runnable, withSafety } from "./steps.js";
|
|
132
133
|
export { changeStoreSetting, isStoreSetting, STORE_SETTINGS, storeSettings } from "./store-settings.js";
|
|
133
|
-
export { type UpgradeContext, upgradeCommand } from "./upgrade-command.js";
|
|
134
|
+
export { restartServers, type UpgradeContext, upgradeCommand } from "./upgrade-command.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -122,7 +122,8 @@ export { createProgram, run } from "./program.js";
|
|
|
122
122
|
export { fieldsOf, projectFields } from "./result-fields.js";
|
|
123
123
|
export { runsCommand } from "./runs/command.js";
|
|
124
124
|
export { count, first, flag, fromFile, NONE_NAMED, plain, settingsFor, } from "./settings.js";
|
|
125
|
+
export { AGENTS, agentFor, screenFor } from "./setup-screen.js";
|
|
125
126
|
export { skillCommand } from "./skill-command.js";
|
|
126
127
|
export { runnable, withSafety } from "./steps.js";
|
|
127
128
|
export { changeStoreSetting, isStoreSetting, STORE_SETTINGS, storeSettings } from "./store-settings.js";
|
|
128
|
-
export { upgradeCommand } from "./upgrade-command.js";
|
|
129
|
+
export { restartServers, upgradeCommand } from "./upgrade-command.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Renderer, type RenderFormat, type Streams } from "@wirecat/cli-base";
|
|
2
|
+
import { type SecretInput } from "../terminal/prompt.js";
|
|
3
|
+
export declare const AGENTS: readonly ["none", "codex", "cursor", "claude", "gemini", "all"];
|
|
4
|
+
export type Agent = (typeof AGENTS)[number];
|
|
5
|
+
/**
|
|
6
|
+
* A person sees each step as a heading with its details indented under it. `--quiet` hides both, as it
|
|
7
|
+
* hides notes; a machine mode keeps the plain notes it always had, since stderr there is a log.
|
|
8
|
+
*/
|
|
9
|
+
export declare const screenFor: (context: {
|
|
10
|
+
format: RenderFormat;
|
|
11
|
+
streams: Pick<Streams, "diagnostic">;
|
|
12
|
+
renderer: Pick<Renderer, "note">;
|
|
13
|
+
}, quiet: boolean, steps: number) => {
|
|
14
|
+
step: (index: number, title: string) => void;
|
|
15
|
+
detail: (text: string) => void;
|
|
16
|
+
title: (text: string) => void;
|
|
17
|
+
indent: number;
|
|
18
|
+
};
|
|
19
|
+
/** Asks only a person at a terminal; every other run gets "none", so no agent is ever installed unasked. */
|
|
20
|
+
export declare const agentFor: (context: {
|
|
21
|
+
format: RenderFormat;
|
|
22
|
+
stdin: NodeJS.ReadableStream & {
|
|
23
|
+
isTTY?: boolean;
|
|
24
|
+
};
|
|
25
|
+
}, given: Agent | undefined, pad: number, prompt?: Pick<SecretInput, "input" | "output" | "signal">) => Promise<Agent>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { CliError, indent } from "@wirecat/cli-base";
|
|
2
|
+
import { readSecret } from "../terminal/prompt.js";
|
|
3
|
+
import { inputPolicy } from "./input-policy.js";
|
|
4
|
+
export const AGENTS = ["none", "codex", "cursor", "claude", "gemini", "all"];
|
|
5
|
+
const DETAIL = 6;
|
|
6
|
+
/**
|
|
7
|
+
* A person sees each step as a heading with its details indented under it. `--quiet` hides both, as it
|
|
8
|
+
* hides notes; a machine mode keeps the plain notes it always had, since stderr there is a log.
|
|
9
|
+
*/
|
|
10
|
+
export const screenFor = (context, quiet, steps) => {
|
|
11
|
+
const person = context.format === "pretty";
|
|
12
|
+
const say = (text) => {
|
|
13
|
+
if (!quiet)
|
|
14
|
+
context.streams.diagnostic(text);
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
step: (index, title) => person ? say(`\n[${index}/${steps}] ${title}`) : context.renderer.note(`${index}/${steps} — ${title}`),
|
|
18
|
+
detail: (text) => (person ? say(indent(text, DETAIL)) : context.renderer.note(text)),
|
|
19
|
+
title: (text) => (person ? say(text) : context.renderer.note(text)),
|
|
20
|
+
indent: person ? DETAIL : 0,
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
/** Asks only a person at a terminal; every other run gets "none", so no agent is ever installed unasked. */
|
|
24
|
+
export const agentFor = async (context, given, pad, prompt = {}) => {
|
|
25
|
+
if (given)
|
|
26
|
+
return given;
|
|
27
|
+
if (context.format !== "pretty" || !context.stdin.isTTY || inputPolicy(context.stdin).noInput)
|
|
28
|
+
return "none";
|
|
29
|
+
const answer = (await readSecret(`${" ".repeat(pad)}Agent [codex/cursor/claude/gemini/all/none] (none): `, {
|
|
30
|
+
...prompt,
|
|
31
|
+
echo: true,
|
|
32
|
+
}))
|
|
33
|
+
.trim()
|
|
34
|
+
.toLowerCase() || "none";
|
|
35
|
+
const agent = AGENTS.find((known) => known === answer);
|
|
36
|
+
if (agent === undefined)
|
|
37
|
+
throw new CliError("validation_error", `unknown agent — choose ${AGENTS.join(", ")} with --agent`);
|
|
38
|
+
return agent;
|
|
39
|
+
};
|
|
@@ -7,4 +7,13 @@ export interface UpgradeContext extends Omit<PackageUpgradePorts, "onInstall"> {
|
|
|
7
7
|
format: RenderFormat;
|
|
8
8
|
streams: Streams;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* A running serve keeps the code it started with; the update replaced the files under it. Each is restarted by
|
|
12
|
+
* the new `script` — `server restart` refuses a serve started by hand, which is then named in `left`.
|
|
13
|
+
* `spawn` replaces the child process, as in `UpdateEnvironment`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const restartServers: (app: AppIdentity, script: string, env: NodeJS.ProcessEnv, spawn?: (argv: string[]) => number) => {
|
|
16
|
+
restarted: string[];
|
|
17
|
+
left: string[];
|
|
18
|
+
};
|
|
10
19
|
export declare const upgradeCommand: (app: AppIdentity, packageName: string, contextFor: (command: Command) => UpgradeContext) => Command;
|
|
@@ -1,9 +1,35 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { CliError } from "@wirecat/cli-base";
|
|
2
3
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
4
|
import { Command } from "commander";
|
|
5
|
+
import { servingProfiles } from "../background/lock.js";
|
|
4
6
|
import { upgradePackage } from "../services/package-upgrade.js";
|
|
5
7
|
import { grantsAuthority, LOCAL_WRITE } from "./effects.js";
|
|
6
8
|
import { itemOf, renderAnswer, snakeKeys } from "./envelope.js";
|
|
9
|
+
import { usableProfileName } from "./profile.js";
|
|
10
|
+
/**
|
|
11
|
+
* A running serve keeps the code it started with; the update replaced the files under it. Each is restarted by
|
|
12
|
+
* the new `script` — `server restart` refuses a serve started by hand, which is then named in `left`.
|
|
13
|
+
* `spawn` replaces the child process, as in `UpdateEnvironment`.
|
|
14
|
+
*/
|
|
15
|
+
export const restartServers = (app, script, env, spawn) => {
|
|
16
|
+
const restarted = [];
|
|
17
|
+
const left = [];
|
|
18
|
+
for (const profile of servingProfiles(app, env)) {
|
|
19
|
+
try {
|
|
20
|
+
usableProfileName(profile);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const argv = [script, profile, "server", "restart"];
|
|
26
|
+
const code = spawn
|
|
27
|
+
? spawn([process.execPath, ...argv])
|
|
28
|
+
: (spawnSync(process.execPath, argv, { env, shell: false, stdio: ["ignore", 2, 2] }).status ?? 1);
|
|
29
|
+
(code === 0 ? restarted : left).push(profile);
|
|
30
|
+
}
|
|
31
|
+
return { restarted, left };
|
|
32
|
+
};
|
|
7
33
|
export const upgradeCommand = (app, packageName, contextFor) => grantsAuthority(annotate(new Command("upgrade"), {
|
|
8
34
|
effects: LOCAL_WRITE,
|
|
9
35
|
mcp: false,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/** Where each test run records which commands and options it drove (`pnpm test:matrix`). */
|
|
4
|
+
export const ARGV_LOG = join(process.cwd(), "coverage", "argv.jsonl");
|
|
5
|
+
/** A vitest `globalSetup`: one run, one log. */
|
|
6
|
+
export const argvLogSetup = () => {
|
|
7
|
+
mkdirSync(join(process.cwd(), "coverage"), { recursive: true });
|
|
8
|
+
rmSync(ARGV_LOG, { force: true });
|
|
9
|
+
};
|
package/dist/kit/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { ARGV_LOG, argvLogSetup } from "./argv-log.js";
|
|
1
2
|
export { type ContractCase, type ContractOptions, contractCases, OPTIONAL_METHODS } from "./contract.js";
|
|
2
3
|
export { type FakeAdapter, fakeAdapter } from "./fake.js";
|
|
3
4
|
export { type McpStdio, type McpStdioStreams, type McpToolResult, mcpOverStdio } from "./mcp-stdio.js";
|
package/dist/kit/index.js
CHANGED
package/dist/store/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export { openCache } from "./open.js";
|
|
|
16
16
|
export { storePath } from "./path.js";
|
|
17
17
|
export { decodeProjectAnnotations, PROJECT_ANNOTATIONS_MAX_BYTES, type ProjectAnnotations, projectAnnotationsFingerprint, } from "./project-transfer/manifest.js";
|
|
18
18
|
export { type ProjectAnnotationsPlan, type ProjectAnnotationsTarget, planProjectAnnotations, } from "./project-transfer/plan.js";
|
|
19
|
+
export { decodeProjectAnnotationSnapshot, type ProjectAnnotationSnapshot, type ProjectAnnotationSnapshotOptions, type ProjectAnnotationSnapshotResult, projectAnnotationSnapshotFingerprint, snapshotProjectAnnotations, } from "./project-transfer/snapshot.js";
|
|
19
20
|
export type { AgentAction, AgentActionInput, AgentActionsStore } from "./sqlite/agent-actions.js";
|
|
20
21
|
export { resetAttachmentWords } from "./sqlite/attachment-texts.js";
|
|
21
22
|
export { backfillNormalized, pendingNormalization } from "./sqlite/backfill.js";
|
package/dist/store/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { openCache } from "./open.js";
|
|
|
8
8
|
export { storePath } from "./path.js";
|
|
9
9
|
export { decodeProjectAnnotations, PROJECT_ANNOTATIONS_MAX_BYTES, projectAnnotationsFingerprint, } from "./project-transfer/manifest.js";
|
|
10
10
|
export { planProjectAnnotations, } from "./project-transfer/plan.js";
|
|
11
|
+
export { decodeProjectAnnotationSnapshot, projectAnnotationSnapshotFingerprint, snapshotProjectAnnotations, } from "./project-transfer/snapshot.js";
|
|
11
12
|
export { resetAttachmentWords } from "./sqlite/attachment-texts.js";
|
|
12
13
|
export { backfillNormalized, pendingNormalization } from "./sqlite/backfill.js";
|
|
13
14
|
export { CATALOG_QUERY } from "./sqlite/catalog.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ProjectAnnotations } from "./schema.js";
|
|
2
2
|
export type { ProjectAnnotations } from "./schema.js";
|
|
3
3
|
export declare const PROJECT_ANNOTATIONS_MAX_BYTES: number;
|
|
4
|
+
export declare const validateProjectAnnotationsGraph: (manifest: ProjectAnnotations, additionalActors?: string[]) => void;
|
|
4
5
|
/** Decode bounded JSON, never a row/column importer or an implicit whole-project backup. */
|
|
5
6
|
export declare const decodeProjectAnnotations: (json: string) => ProjectAnnotations;
|
|
6
7
|
export declare const projectAnnotationsFingerprint: (json: string) => string;
|
|
@@ -17,7 +17,7 @@ const canonical = (ref) => {
|
|
|
17
17
|
return invalid("reference");
|
|
18
18
|
}
|
|
19
19
|
};
|
|
20
|
-
const
|
|
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
|
-
|
|
128
|
+
validateProjectAnnotationsGraph(manifest);
|
|
125
129
|
return manifest;
|
|
126
130
|
};
|
|
127
131
|
export const projectAnnotationsFingerprint = (json) => {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
import { type ProjectAnnotations } from "./schema.js";
|
|
3
|
+
declare const schema: v.StrictObjectSchema<{
|
|
4
|
+
readonly format: v.LiteralSchema<"wirecat.project-annotations", undefined>;
|
|
5
|
+
readonly facts: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
|
|
6
|
+
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
7
|
+
readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
|
|
8
|
+
readonly statement: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
9
|
+
readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed"], undefined>;
|
|
10
|
+
readonly subject: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
11
|
+
readonly occurred_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
12
|
+
readonly supersedes: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
13
|
+
readonly author: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
14
|
+
readonly model: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
15
|
+
readonly confirmed_by: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
16
|
+
readonly scope: v.NullableSchema<v.PicklistSchema<readonly ["personal", "work"], undefined>, undefined>;
|
|
17
|
+
readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
18
|
+
readonly created_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
19
|
+
readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
20
|
+
}, undefined>, undefined>, v.MaxLengthAction<{
|
|
21
|
+
ref: string;
|
|
22
|
+
type: "decision" | "fact" | "promise" | "question" | "statement";
|
|
23
|
+
statement: string;
|
|
24
|
+
status: "accepted" | "proposed" | "reversed" | "superseded";
|
|
25
|
+
subject: string | null;
|
|
26
|
+
occurred_at: number | null;
|
|
27
|
+
supersedes: string | null;
|
|
28
|
+
author: string;
|
|
29
|
+
model: string | null;
|
|
30
|
+
confirmed_by: string | null;
|
|
31
|
+
scope: "personal" | "work" | null;
|
|
32
|
+
source: string;
|
|
33
|
+
created_at: number;
|
|
34
|
+
updated_at: number;
|
|
35
|
+
}[], 1000, undefined>]>;
|
|
36
|
+
readonly links: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
|
|
37
|
+
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
38
|
+
readonly from: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
39
|
+
readonly to: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
40
|
+
readonly kind: v.PicklistSchema<readonly ["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to"], undefined>;
|
|
41
|
+
readonly origin: v.PicklistSchema<readonly ["file", "owner", "suggested", "message"], undefined>;
|
|
42
|
+
readonly anchor: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
43
|
+
readonly target_text: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
44
|
+
readonly role: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
45
|
+
readonly evidence: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
46
|
+
readonly confirmed: v.BooleanSchema<undefined>;
|
|
47
|
+
readonly provenance: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
48
|
+
readonly created_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
49
|
+
readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
50
|
+
readonly author: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
51
|
+
}, undefined>, undefined>, v.MaxLengthAction<{
|
|
52
|
+
ref: string;
|
|
53
|
+
from: string;
|
|
54
|
+
to: string | null;
|
|
55
|
+
kind: "about" | "answered-by" | "assigned-to" | "blocked-by" | "created-from" | "duplicate-of" | "evidence" | "follow-up-of" | "labelled" | "links-to" | "member-of" | "related-to";
|
|
56
|
+
origin: "file" | "message" | "owner" | "suggested";
|
|
57
|
+
anchor: string | null;
|
|
58
|
+
target_text: string | null;
|
|
59
|
+
role: string | null;
|
|
60
|
+
evidence: string | null;
|
|
61
|
+
confirmed: boolean;
|
|
62
|
+
provenance: string | null;
|
|
63
|
+
created_at: number;
|
|
64
|
+
updated_at: number;
|
|
65
|
+
author: string | null;
|
|
66
|
+
}[], 1000, undefined>]>;
|
|
67
|
+
readonly external: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
|
|
68
|
+
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
69
|
+
readonly content_sha256: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>;
|
|
70
|
+
}, undefined>, undefined>, v.MaxLengthAction<{
|
|
71
|
+
ref: string;
|
|
72
|
+
content_sha256: string | null;
|
|
73
|
+
}[], 1000, undefined>]>;
|
|
74
|
+
readonly version: v.LiteralSchema<2, undefined>;
|
|
75
|
+
readonly coverage: v.StrictObjectSchema<{
|
|
76
|
+
readonly aliases: v.LiteralSchema<"complete", undefined>;
|
|
77
|
+
readonly facts: v.LiteralSchema<"complete", undefined>;
|
|
78
|
+
readonly links: v.LiteralSchema<"complete", undefined>;
|
|
79
|
+
readonly memories: v.LiteralSchema<"omitted", undefined>;
|
|
80
|
+
readonly tasks: v.LiteralSchema<"omitted", undefined>;
|
|
81
|
+
readonly labels: v.LiteralSchema<"omitted", undefined>;
|
|
82
|
+
readonly findings: v.LiteralSchema<"omitted", undefined>;
|
|
83
|
+
readonly meeting_memberships: v.LiteralSchema<"omitted", undefined>;
|
|
84
|
+
readonly notes: v.LiteralSchema<"omitted", undefined>;
|
|
85
|
+
}, undefined>;
|
|
86
|
+
readonly project: v.StrictObjectSchema<{
|
|
87
|
+
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
88
|
+
readonly key: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>;
|
|
89
|
+
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
90
|
+
readonly type: v.PicklistSchema<["work", "client", "personal", "oss", "other"], undefined>;
|
|
91
|
+
readonly scope: v.PicklistSchema<readonly ["personal", "work"], undefined>;
|
|
92
|
+
readonly organization: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
93
|
+
readonly created_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
94
|
+
readonly description: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>, v.CheckAction<string, undefined>]>, undefined>;
|
|
95
|
+
readonly status: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>, v.CheckAction<string, undefined>]>, undefined>;
|
|
96
|
+
readonly owner: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>, v.CheckAction<string, undefined>]>, undefined>;
|
|
97
|
+
readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
98
|
+
readonly account_scope: v.LiteralSchema<"owner", undefined>;
|
|
99
|
+
}, undefined>;
|
|
100
|
+
readonly aliases: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
|
|
101
|
+
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>, v.CheckAction<string, undefined>]>;
|
|
102
|
+
readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>, v.CheckAction<string, undefined>]>;
|
|
103
|
+
readonly display: v.BooleanSchema<undefined>;
|
|
104
|
+
readonly created_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
105
|
+
readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
106
|
+
}, undefined>, undefined>, v.MaxLengthAction<{
|
|
107
|
+
name: string;
|
|
108
|
+
source: string;
|
|
109
|
+
display: boolean;
|
|
110
|
+
created_at: number;
|
|
111
|
+
updated_at: number;
|
|
112
|
+
}[], 1000, undefined>]>;
|
|
113
|
+
}, undefined>;
|
|
114
|
+
export type ProjectAnnotationSnapshot = v.InferOutput<typeof schema>;
|
|
115
|
+
export declare const snapshotGraph: (value: ProjectAnnotationSnapshot) => ProjectAnnotations;
|
|
116
|
+
export declare const decodeProjectAnnotationSnapshot: (json: string) => ProjectAnnotationSnapshot;
|
|
117
|
+
export declare const projectAnnotationSnapshotFingerprint: (json: string) => string;
|
|
118
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { CliError } from "@wirecat/cli-base";
|
|
3
|
+
import * as v from "valibot";
|
|
4
|
+
import { PROJECT_ANNOTATIONS_MAX_BYTES, validateProjectAnnotationsGraph } from "./manifest.js";
|
|
5
|
+
import { projectAnnotationsSchema } from "./schema.js";
|
|
6
|
+
const text = (max) => v.pipe(v.string(), v.minLength(1), v.maxLength(max), v.check((s) => !!s.trim()));
|
|
7
|
+
const timestamp = v.pipe(v.number(), v.safeInteger(), v.minValue(0));
|
|
8
|
+
const schema = v.strictObject({
|
|
9
|
+
...projectAnnotationsSchema.entries,
|
|
10
|
+
version: v.literal(2),
|
|
11
|
+
coverage: v.strictObject({
|
|
12
|
+
...projectAnnotationsSchema.entries.coverage.entries,
|
|
13
|
+
meeting_memberships: v.literal("omitted"),
|
|
14
|
+
notes: v.literal("omitted"),
|
|
15
|
+
}),
|
|
16
|
+
project: v.strictObject({
|
|
17
|
+
...projectAnnotationsSchema.entries.project.entries,
|
|
18
|
+
description: v.nullable(text(20000)),
|
|
19
|
+
status: v.nullable(text(200)),
|
|
20
|
+
owner: v.nullable(text(2048)),
|
|
21
|
+
updated_at: timestamp,
|
|
22
|
+
account_scope: v.literal("owner"),
|
|
23
|
+
}),
|
|
24
|
+
aliases: v.pipe(v.array(v.strictObject({
|
|
25
|
+
name: text(200),
|
|
26
|
+
source: text(200),
|
|
27
|
+
display: v.boolean(),
|
|
28
|
+
created_at: timestamp,
|
|
29
|
+
updated_at: timestamp,
|
|
30
|
+
})), v.maxLength(1000)),
|
|
31
|
+
});
|
|
32
|
+
export const snapshotGraph = (value) => ({
|
|
33
|
+
format: value.format,
|
|
34
|
+
version: 1,
|
|
35
|
+
project: {
|
|
36
|
+
ref: value.project.ref,
|
|
37
|
+
key: value.project.key,
|
|
38
|
+
name: value.project.name,
|
|
39
|
+
type: value.project.type,
|
|
40
|
+
scope: value.project.scope,
|
|
41
|
+
organization: value.project.organization,
|
|
42
|
+
created_at: value.project.created_at,
|
|
43
|
+
},
|
|
44
|
+
coverage: value.coverage,
|
|
45
|
+
aliases: value.aliases.map((alias) => alias.name),
|
|
46
|
+
facts: value.facts,
|
|
47
|
+
links: value.links,
|
|
48
|
+
external: value.external,
|
|
49
|
+
});
|
|
50
|
+
export const decodeProjectAnnotationSnapshot = (json) => {
|
|
51
|
+
const invalid = (field) => {
|
|
52
|
+
throw new CliError("validation_error", `invalid project annotation snapshot.${field}`);
|
|
53
|
+
};
|
|
54
|
+
if (typeof json !== "string" || Buffer.byteLength(json) > PROJECT_ANNOTATIONS_MAX_BYTES)
|
|
55
|
+
return invalid("bytes");
|
|
56
|
+
let value;
|
|
57
|
+
try {
|
|
58
|
+
value = JSON.parse(json);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return invalid("json");
|
|
62
|
+
}
|
|
63
|
+
const checked = v.safeParse(schema, value);
|
|
64
|
+
if (!checked.success)
|
|
65
|
+
return invalid(checked.issues[0]?.path?.map((p) => String(p.key)).join(".") ?? "manifest");
|
|
66
|
+
const snapshot = checked.output;
|
|
67
|
+
if (snapshot.aliases.length + snapshot.facts.length + snapshot.links.length + snapshot.external.length > 1000)
|
|
68
|
+
return invalid("record budget");
|
|
69
|
+
if (snapshot.project.updated_at < snapshot.project.created_at)
|
|
70
|
+
return invalid("project.updated_at");
|
|
71
|
+
for (const alias of snapshot.aliases)
|
|
72
|
+
if (alias.updated_at < alias.created_at)
|
|
73
|
+
return invalid("aliases.updated_at");
|
|
74
|
+
for (const fact of snapshot.facts)
|
|
75
|
+
if (!fact.statement.trim())
|
|
76
|
+
return invalid("facts.statement");
|
|
77
|
+
validateProjectAnnotationsGraph(snapshotGraph(snapshot), snapshot.project.owner === null ? [] : [snapshot.project.owner]);
|
|
78
|
+
return snapshot;
|
|
79
|
+
};
|
|
80
|
+
export const projectAnnotationSnapshotFingerprint = (json) => {
|
|
81
|
+
const snapshot = decodeProjectAnnotationSnapshot(json);
|
|
82
|
+
const order = (a, b) => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0);
|
|
83
|
+
return createHash("sha256")
|
|
84
|
+
.update(JSON.stringify({
|
|
85
|
+
...snapshot,
|
|
86
|
+
aliases: snapshot.aliases.toSorted((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)),
|
|
87
|
+
facts: snapshot.facts.toSorted(order),
|
|
88
|
+
links: snapshot.links.toSorted(order),
|
|
89
|
+
external: snapshot.external.toSorted(order),
|
|
90
|
+
}))
|
|
91
|
+
.digest("hex");
|
|
92
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ProjectAnnotationSnapshot } from "./snapshot-model.js";
|
|
2
|
+
export type { ProjectAnnotationSnapshot } from "./snapshot-model.js";
|
|
3
|
+
export { decodeProjectAnnotationSnapshot, projectAnnotationSnapshotFingerprint } from "./snapshot-model.js";
|
|
4
|
+
export interface ProjectAnnotationSnapshotOptions {
|
|
5
|
+
source: string;
|
|
6
|
+
project: string;
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
maxReadBytes?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ProjectAnnotationSnapshotResult {
|
|
11
|
+
snapshot: ProjectAnnotationSnapshot;
|
|
12
|
+
fingerprint: string;
|
|
13
|
+
read_bytes: number;
|
|
14
|
+
records: number;
|
|
15
|
+
}
|
|
16
|
+
/** No source maintenance, source fetching or output-file publication belongs to a snapshot read. */
|
|
17
|
+
export declare const snapshotProjectAnnotations: (options: ProjectAnnotationSnapshotOptions) => Promise<ProjectAnnotationSnapshotResult>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { openStoreContext } from "../sqlite/open-store.js";
|
|
3
|
+
import { SnapshotBudget } from "../sqlite/project-transfer/budget.js";
|
|
4
|
+
import { projectAnnotationSnapshot } from "../sqlite/project-transfer/snapshot.js";
|
|
5
|
+
import { projectAnnotationSnapshotFingerprint } from "./snapshot-model.js";
|
|
6
|
+
export { decodeProjectAnnotationSnapshot, projectAnnotationSnapshotFingerprint } from "./snapshot-model.js";
|
|
7
|
+
/** No source maintenance, source fetching or output-file publication belongs to a snapshot read. */
|
|
8
|
+
export const snapshotProjectAnnotations = async (options) => {
|
|
9
|
+
if (!options || typeof options !== "object" || Array.isArray(options))
|
|
10
|
+
throw new CliError("validation_error", "snapshot options are required");
|
|
11
|
+
for (const field of Object.keys(options))
|
|
12
|
+
if (!["source", "project", "env", "maxReadBytes"].includes(field))
|
|
13
|
+
throw new CliError("validation_error", `unknown snapshot field ${field}`);
|
|
14
|
+
if (typeof options.source !== "string" || !options.source.trim() || options.source.length > 4096)
|
|
15
|
+
throw new CliError("validation_error", "snapshot source is an explicit existing store path");
|
|
16
|
+
if (typeof options.project !== "string" ||
|
|
17
|
+
!/^project:[1-9]\d*$/.test(options.project) ||
|
|
18
|
+
!Number.isSafeInteger(Number(options.project.slice(8))))
|
|
19
|
+
throw new CliError("validation_error", "snapshot project is a project:<safe integer id> reference");
|
|
20
|
+
const budget = new SnapshotBudget(options.maxReadBytes ?? 4 * 1024 * 1024);
|
|
21
|
+
const context = await openStoreContext({ path: options.source, env: options.env, readOnly: true });
|
|
22
|
+
try {
|
|
23
|
+
const result = projectAnnotationSnapshot(context.database, Number(options.project.slice(8)), budget);
|
|
24
|
+
return { ...result, fingerprint: projectAnnotationSnapshotFingerprint(JSON.stringify(result.snapshot)) };
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
context.database.close();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CacheDatabase, SqlValue } from "../../driver.js";
|
|
2
|
+
export declare class SnapshotBudget {
|
|
3
|
+
readonly maxBytes: number;
|
|
4
|
+
records: number;
|
|
5
|
+
bytes: number;
|
|
6
|
+
constructor(maxBytes: number);
|
|
7
|
+
reserve(records: number, bytes: number): void;
|
|
8
|
+
rows(database: CacheDatabase, from: string, fields: string[], parameters: SqlValue[]): void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
export class SnapshotBudget {
|
|
3
|
+
maxBytes;
|
|
4
|
+
records = 0;
|
|
5
|
+
bytes = 0;
|
|
6
|
+
constructor(maxBytes) {
|
|
7
|
+
this.maxBytes = maxBytes;
|
|
8
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 4 * 1024 * 1024)
|
|
9
|
+
throw new CliError("validation_error", "snapshot maxReadBytes takes 1–4194304 bytes");
|
|
10
|
+
}
|
|
11
|
+
reserve(records, bytes) {
|
|
12
|
+
if (!Number.isSafeInteger(records) ||
|
|
13
|
+
records < 0 ||
|
|
14
|
+
!Number.isSafeInteger(bytes) ||
|
|
15
|
+
bytes < 0 ||
|
|
16
|
+
this.records + records > 1000 ||
|
|
17
|
+
this.bytes + bytes > this.maxBytes)
|
|
18
|
+
throw new CliError("validation_error", "project annotation snapshot exceeds its record or input byte budget");
|
|
19
|
+
this.records += records;
|
|
20
|
+
this.bytes += bytes;
|
|
21
|
+
}
|
|
22
|
+
rows(database, from, fields, parameters) {
|
|
23
|
+
const bytes = fields.map((field) => `coalesce(length(CAST(${field} AS BLOB)),0)`).join("+");
|
|
24
|
+
const sized = database.prepare(`SELECT ${bytes} AS bytes ${from} LIMIT 1001`).all(...parameters);
|
|
25
|
+
this.reserve(sized.length, sized.reduce((sum, row) => sum + Number(row.bytes) + 128, 0));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { CacheDatabase } from "../../driver.js";
|
|
2
|
+
import type { SnapshotBudget } from "./budget.js";
|
|
3
|
+
export declare const projectAnnotationSnapshot: (database: CacheDatabase, projectId: number, budget: SnapshotBudget) => {
|
|
4
|
+
snapshot: {
|
|
5
|
+
format: "wirecat.project-annotations";
|
|
6
|
+
facts: {
|
|
7
|
+
ref: string;
|
|
8
|
+
type: "decision" | "fact" | "promise" | "question" | "statement";
|
|
9
|
+
statement: string;
|
|
10
|
+
status: "accepted" | "proposed" | "reversed" | "superseded";
|
|
11
|
+
subject: string | null;
|
|
12
|
+
occurred_at: number | null;
|
|
13
|
+
supersedes: string | null;
|
|
14
|
+
author: string;
|
|
15
|
+
model: string | null;
|
|
16
|
+
confirmed_by: string | null;
|
|
17
|
+
scope: "personal" | "work" | null;
|
|
18
|
+
source: string;
|
|
19
|
+
created_at: number;
|
|
20
|
+
updated_at: number;
|
|
21
|
+
}[];
|
|
22
|
+
links: {
|
|
23
|
+
ref: string;
|
|
24
|
+
from: string;
|
|
25
|
+
to: string | null;
|
|
26
|
+
kind: "about" | "answered-by" | "assigned-to" | "blocked-by" | "created-from" | "duplicate-of" | "evidence" | "follow-up-of" | "labelled" | "links-to" | "member-of" | "related-to";
|
|
27
|
+
origin: "file" | "message" | "owner" | "suggested";
|
|
28
|
+
anchor: string | null;
|
|
29
|
+
target_text: string | null;
|
|
30
|
+
role: string | null;
|
|
31
|
+
evidence: string | null;
|
|
32
|
+
confirmed: boolean;
|
|
33
|
+
provenance: string | null;
|
|
34
|
+
created_at: number;
|
|
35
|
+
updated_at: number;
|
|
36
|
+
author: string | null;
|
|
37
|
+
}[];
|
|
38
|
+
external: {
|
|
39
|
+
ref: string;
|
|
40
|
+
content_sha256: string | null;
|
|
41
|
+
}[];
|
|
42
|
+
version: 2;
|
|
43
|
+
coverage: {
|
|
44
|
+
aliases: "complete";
|
|
45
|
+
facts: "complete";
|
|
46
|
+
links: "complete";
|
|
47
|
+
memories: "omitted";
|
|
48
|
+
tasks: "omitted";
|
|
49
|
+
labels: "omitted";
|
|
50
|
+
findings: "omitted";
|
|
51
|
+
meeting_memberships: "omitted";
|
|
52
|
+
notes: "omitted";
|
|
53
|
+
};
|
|
54
|
+
project: {
|
|
55
|
+
ref: string;
|
|
56
|
+
key: string;
|
|
57
|
+
name: string;
|
|
58
|
+
type: "client" | "oss" | "other" | "personal" | "work";
|
|
59
|
+
scope: "personal" | "work";
|
|
60
|
+
organization: string | null;
|
|
61
|
+
created_at: number;
|
|
62
|
+
description: string | null;
|
|
63
|
+
status: string | null;
|
|
64
|
+
owner: string | null;
|
|
65
|
+
updated_at: number;
|
|
66
|
+
account_scope: "owner";
|
|
67
|
+
};
|
|
68
|
+
aliases: {
|
|
69
|
+
name: string;
|
|
70
|
+
source: string;
|
|
71
|
+
display: boolean;
|
|
72
|
+
created_at: number;
|
|
73
|
+
updated_at: number;
|
|
74
|
+
}[];
|
|
75
|
+
};
|
|
76
|
+
read_bytes: number;
|
|
77
|
+
records: number;
|
|
78
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { MIGRATIONS } from "../../migrations.js";
|
|
3
|
+
import { decodeProjectAnnotationSnapshot } from "../../project-transfer/snapshot-model.js";
|
|
4
|
+
import { read } from "../transaction.js";
|
|
5
|
+
import { ProjectSnapshotSources } from "./sources.js";
|
|
6
|
+
const unsupported = () => {
|
|
7
|
+
throw new CliError("configuration_error", "project snapshot has unsupported metadata, deletion or account scope");
|
|
8
|
+
};
|
|
9
|
+
const nullableText = (value) => (value === null ? null : String(value));
|
|
10
|
+
const flag = (value) => {
|
|
11
|
+
if (value !== 0 && value !== 1)
|
|
12
|
+
return unsupported();
|
|
13
|
+
return value === 1;
|
|
14
|
+
};
|
|
15
|
+
const provenance = (raw) => {
|
|
16
|
+
if (raw === null)
|
|
17
|
+
return null;
|
|
18
|
+
let value;
|
|
19
|
+
try {
|
|
20
|
+
value = JSON.parse(String(raw));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return unsupported();
|
|
24
|
+
}
|
|
25
|
+
if (!value ||
|
|
26
|
+
typeof value !== "object" ||
|
|
27
|
+
Array.isArray(value) ||
|
|
28
|
+
Object.keys(value).some((key) => key !== "provenance"))
|
|
29
|
+
return unsupported();
|
|
30
|
+
const text = value.provenance;
|
|
31
|
+
if (text !== undefined && text !== null && typeof text !== "string")
|
|
32
|
+
return unsupported();
|
|
33
|
+
return text ?? null;
|
|
34
|
+
};
|
|
35
|
+
export const projectAnnotationSnapshot = (database, projectId, budget) => read(database, () => {
|
|
36
|
+
if (database.prepare("SELECT max(version) AS version FROM schema_migrations").get()?.version !==
|
|
37
|
+
MIGRATIONS.at(-1)?.version)
|
|
38
|
+
throw new CliError("configuration_error", "project snapshots require the SDK's current store schema without source migration");
|
|
39
|
+
const sources = new ProjectSnapshotSources(database, budget);
|
|
40
|
+
const rows = (from, fields, params) => {
|
|
41
|
+
budget.rows(database, from, fields, params);
|
|
42
|
+
return database.prepare(`SELECT ${fields.join(",")} ${from} LIMIT 1001`).all(...params);
|
|
43
|
+
};
|
|
44
|
+
const descriptor = database.prepare("SELECT account_id,deleted_at FROM projects WHERE id=?").get(projectId);
|
|
45
|
+
if (!descriptor)
|
|
46
|
+
throw new CliError("not_found", "the source project does not exist");
|
|
47
|
+
if (descriptor.account_id !== null || descriptor.deleted_at !== null)
|
|
48
|
+
return unsupported();
|
|
49
|
+
const project = rows("FROM projects WHERE id=?", [
|
|
50
|
+
"id",
|
|
51
|
+
"key",
|
|
52
|
+
"name",
|
|
53
|
+
"description",
|
|
54
|
+
"type",
|
|
55
|
+
"scope",
|
|
56
|
+
"organization_id",
|
|
57
|
+
"owner_type",
|
|
58
|
+
"owner_id",
|
|
59
|
+
"status",
|
|
60
|
+
"created_at",
|
|
61
|
+
"updated_at",
|
|
62
|
+
], [projectId])[0];
|
|
63
|
+
if (!project)
|
|
64
|
+
throw new CliError("not_found", "the source project does not exist");
|
|
65
|
+
const aliases = rows("FROM aliases WHERE aliasable_type='project' AND aliasable_id=? ORDER BY id", ["name", "source", "display", "account_id", "created_at", "updated_at"], [projectId]);
|
|
66
|
+
if (aliases.some((alias) => alias.account_id !== null))
|
|
67
|
+
return unsupported();
|
|
68
|
+
if (database
|
|
69
|
+
.prepare("SELECT 1 FROM facts WHERE project_id=? AND (metadata IS NOT NULL OR deleted_at IS NOT NULL) LIMIT 1")
|
|
70
|
+
.get(projectId))
|
|
71
|
+
return unsupported();
|
|
72
|
+
const facts = rows("FROM facts WHERE project_id=? ORDER BY id", [
|
|
73
|
+
"id",
|
|
74
|
+
"type",
|
|
75
|
+
"statement",
|
|
76
|
+
"status",
|
|
77
|
+
"subject_type",
|
|
78
|
+
"subject_id",
|
|
79
|
+
"occurred_at",
|
|
80
|
+
"supersedes_id",
|
|
81
|
+
"author_type",
|
|
82
|
+
"author_id",
|
|
83
|
+
"model",
|
|
84
|
+
"confirmed_by_type",
|
|
85
|
+
"confirmed_by_id",
|
|
86
|
+
"scope",
|
|
87
|
+
"source",
|
|
88
|
+
"created_at",
|
|
89
|
+
"updated_at",
|
|
90
|
+
], [projectId]);
|
|
91
|
+
const links = rows("FROM links WHERE (from_type='project' AND from_id=?) OR (to_type='project' AND to_id=?) OR " +
|
|
92
|
+
"(from_type='fact' AND from_id IN (SELECT id FROM facts WHERE project_id=?)) OR " +
|
|
93
|
+
"(to_type='fact' AND to_id IN (SELECT id FROM facts WHERE project_id=?)) ORDER BY id", [
|
|
94
|
+
"id",
|
|
95
|
+
"from_type",
|
|
96
|
+
"from_id",
|
|
97
|
+
"to_type",
|
|
98
|
+
"to_id",
|
|
99
|
+
"kind",
|
|
100
|
+
"anchor",
|
|
101
|
+
"source",
|
|
102
|
+
"target_text",
|
|
103
|
+
"role",
|
|
104
|
+
"evidence",
|
|
105
|
+
"metadata",
|
|
106
|
+
"confirmed",
|
|
107
|
+
"author",
|
|
108
|
+
"created_at",
|
|
109
|
+
"updated_at",
|
|
110
|
+
], [projectId, projectId, projectId, projectId]);
|
|
111
|
+
const projectRef = `project:${projectId}`;
|
|
112
|
+
const owned = new Set([projectRef, ...facts.map((row) => `fact:${row.id}`)]);
|
|
113
|
+
const data = {
|
|
114
|
+
format: "wirecat.project-annotations",
|
|
115
|
+
version: 2,
|
|
116
|
+
project: {
|
|
117
|
+
ref: projectRef,
|
|
118
|
+
key: project.key,
|
|
119
|
+
name: project.name,
|
|
120
|
+
type: project.type,
|
|
121
|
+
scope: project.scope,
|
|
122
|
+
organization: project.organization_id === null ? null : sources.ref("organization", project.organization_id),
|
|
123
|
+
created_at: project.created_at,
|
|
124
|
+
description: project.description,
|
|
125
|
+
status: project.status,
|
|
126
|
+
owner: sources.ref(project.owner_type, project.owner_id),
|
|
127
|
+
updated_at: project.updated_at,
|
|
128
|
+
account_scope: "owner",
|
|
129
|
+
},
|
|
130
|
+
coverage: {
|
|
131
|
+
aliases: "complete",
|
|
132
|
+
facts: "complete",
|
|
133
|
+
links: "complete",
|
|
134
|
+
memories: "omitted",
|
|
135
|
+
tasks: "omitted",
|
|
136
|
+
labels: "omitted",
|
|
137
|
+
findings: "omitted",
|
|
138
|
+
meeting_memberships: "omitted",
|
|
139
|
+
notes: "omitted",
|
|
140
|
+
},
|
|
141
|
+
aliases: aliases.map((row) => ({
|
|
142
|
+
name: row.name,
|
|
143
|
+
source: row.source,
|
|
144
|
+
display: flag(row.display),
|
|
145
|
+
created_at: row.created_at,
|
|
146
|
+
updated_at: row.updated_at,
|
|
147
|
+
})),
|
|
148
|
+
facts: facts.map((row) => ({
|
|
149
|
+
ref: `fact:${row.id}`,
|
|
150
|
+
type: row.type,
|
|
151
|
+
statement: row.statement,
|
|
152
|
+
status: row.status,
|
|
153
|
+
subject: sources.ref(row.subject_type, row.subject_id),
|
|
154
|
+
occurred_at: row.occurred_at,
|
|
155
|
+
supersedes: row.supersedes_id === null ? null : sources.ref("fact", row.supersedes_id),
|
|
156
|
+
author: sources.ref(row.author_type, row.author_id),
|
|
157
|
+
model: row.model,
|
|
158
|
+
confirmed_by: sources.ref(row.confirmed_by_type, row.confirmed_by_id),
|
|
159
|
+
scope: row.scope,
|
|
160
|
+
source: row.source,
|
|
161
|
+
created_at: row.created_at,
|
|
162
|
+
updated_at: row.updated_at,
|
|
163
|
+
})),
|
|
164
|
+
links: links.map((row) => ({
|
|
165
|
+
ref: `link:${row.id}`,
|
|
166
|
+
from: sources.ref(row.from_type, row.from_id),
|
|
167
|
+
to: sources.ref(row.to_type, row.to_id),
|
|
168
|
+
kind: row.kind,
|
|
169
|
+
origin: row.source,
|
|
170
|
+
anchor: nullableText(row.anchor),
|
|
171
|
+
target_text: nullableText(row.target_text),
|
|
172
|
+
role: nullableText(row.role),
|
|
173
|
+
evidence: nullableText(row.evidence),
|
|
174
|
+
confirmed: flag(row.confirmed),
|
|
175
|
+
provenance: provenance(row.metadata),
|
|
176
|
+
created_at: row.created_at,
|
|
177
|
+
updated_at: row.updated_at,
|
|
178
|
+
author: nullableText(row.author),
|
|
179
|
+
})),
|
|
180
|
+
};
|
|
181
|
+
const snapshot = decodeProjectAnnotationSnapshot(JSON.stringify({ ...data, external: sources.external(owned) }));
|
|
182
|
+
return { snapshot, read_bytes: budget.bytes, records: budget.records };
|
|
183
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CacheDatabase } from "../../driver.js";
|
|
2
|
+
import { type Thing } from "../things.js";
|
|
3
|
+
import type { SnapshotBudget } from "./budget.js";
|
|
4
|
+
export declare class ProjectSnapshotSources {
|
|
5
|
+
private readonly database;
|
|
6
|
+
private readonly budget;
|
|
7
|
+
readonly references: Map<string, Thing>;
|
|
8
|
+
private readonly byThing;
|
|
9
|
+
constructor(database: CacheDatabase, budget: SnapshotBudget);
|
|
10
|
+
ref(type: unknown, id: unknown): string | null;
|
|
11
|
+
external(owned: Set<string>): {
|
|
12
|
+
ref: string;
|
|
13
|
+
content_sha256: string | null;
|
|
14
|
+
}[];
|
|
15
|
+
private fingerprint;
|
|
16
|
+
private transcript;
|
|
17
|
+
private digest;
|
|
18
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { CliError } from "@wirecat/cli-base";
|
|
3
|
+
import { referenceOfThing, storedThing } from "../things.js";
|
|
4
|
+
const invalid = () => {
|
|
5
|
+
throw new CliError("configuration_error", "project snapshot has an unavailable or unsupported dependency");
|
|
6
|
+
};
|
|
7
|
+
const scalar = new Set([
|
|
8
|
+
"person",
|
|
9
|
+
"bot",
|
|
10
|
+
"organization",
|
|
11
|
+
"project",
|
|
12
|
+
"fact",
|
|
13
|
+
"memory",
|
|
14
|
+
"task",
|
|
15
|
+
"note",
|
|
16
|
+
"document",
|
|
17
|
+
"message",
|
|
18
|
+
"email",
|
|
19
|
+
"chat",
|
|
20
|
+
"identity",
|
|
21
|
+
"account",
|
|
22
|
+
"meeting",
|
|
23
|
+
"meeting_transcript",
|
|
24
|
+
"meeting_transcript_row",
|
|
25
|
+
]);
|
|
26
|
+
const noText = new Set(["person", "bot", "organization", "project", "chat", "identity", "account"]);
|
|
27
|
+
const texts = {
|
|
28
|
+
document: {
|
|
29
|
+
table: "documents",
|
|
30
|
+
fields: ["n.title", "n.body"],
|
|
31
|
+
where: "n.deleted_at IS NULL AND n.body IS NOT NULL AND CASE WHEN json_valid(n.metadata) THEN coalesce(json_extract(n.metadata,'$.catalog.active'),1) ELSE 1 END=1",
|
|
32
|
+
},
|
|
33
|
+
note: { table: "notes", fields: ["n.title", "n.body"], where: "n.deleted_at IS NULL" },
|
|
34
|
+
message: {
|
|
35
|
+
table: "messages",
|
|
36
|
+
fields: ["n.text", "n.sent_at", "n.sender_name", "n.sender_chat_external_id", "i.provider", "i.external_id"],
|
|
37
|
+
join: "LEFT JOIN identities i ON i.id=n.sender_identity_id",
|
|
38
|
+
where: "n.deleted_at IS NULL",
|
|
39
|
+
},
|
|
40
|
+
email: {
|
|
41
|
+
table: "emails",
|
|
42
|
+
fields: ["n.subject", "coalesce(n.body_text,'')", "n.sent_at", "n.from_address", "n.from_name"],
|
|
43
|
+
where: "n.deleted_at IS NULL",
|
|
44
|
+
},
|
|
45
|
+
meeting: {
|
|
46
|
+
table: "meetings",
|
|
47
|
+
fields: ["n.title", "n.description", "n.location", "n.started_at", "n.ended_at", "n.duration_ms", "n.timezone"],
|
|
48
|
+
where: "n.deleted_at IS NULL",
|
|
49
|
+
},
|
|
50
|
+
meeting_transcript_row: {
|
|
51
|
+
table: "meeting_transcript_rows",
|
|
52
|
+
fields: ["n.position", "n.start_ms", "n.end_ms", "n.speaker_name", "n.text"],
|
|
53
|
+
join: "JOIN meeting_transcripts t ON t.id=n.meeting_transcript_id JOIN meetings m ON m.id=t.meeting_id",
|
|
54
|
+
where: "t.deleted_at IS NULL AND m.deleted_at IS NULL",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
export class ProjectSnapshotSources {
|
|
58
|
+
database;
|
|
59
|
+
budget;
|
|
60
|
+
references = new Map();
|
|
61
|
+
byThing = new Map();
|
|
62
|
+
constructor(database, budget) {
|
|
63
|
+
this.database = database;
|
|
64
|
+
this.budget = budget;
|
|
65
|
+
}
|
|
66
|
+
ref(type, id) {
|
|
67
|
+
if (type === null && id === null)
|
|
68
|
+
return null;
|
|
69
|
+
if (typeof type !== "string" || !scalar.has(type) || typeof id !== "number" || !Number.isSafeInteger(id) || id < 1)
|
|
70
|
+
return invalid();
|
|
71
|
+
const key = `${type}:${id}`, cached = this.byThing.get(key);
|
|
72
|
+
if (cached)
|
|
73
|
+
return cached;
|
|
74
|
+
if (type === "project" || type === "organization") {
|
|
75
|
+
const table = type === "project" ? "projects" : "organizations";
|
|
76
|
+
if (!this.database.prepare(`SELECT 1 FROM ${table} WHERE id=? AND deleted_at IS NULL`).get(id))
|
|
77
|
+
return invalid();
|
|
78
|
+
}
|
|
79
|
+
const identifier = {
|
|
80
|
+
message: [
|
|
81
|
+
"FROM messages n JOIN chats c ON c.id=n.chat_id JOIN accounts a ON a.id=n.account_id WHERE n.id=?",
|
|
82
|
+
["n.external_id", "c.external_id", "a.external_id", "a.provider"],
|
|
83
|
+
],
|
|
84
|
+
email: [
|
|
85
|
+
"FROM emails n JOIN email_threads c ON c.id=n.email_thread_id JOIN accounts a ON a.id=n.account_id WHERE n.id=?",
|
|
86
|
+
["n.external_id", "c.external_id", "a.external_id", "a.provider"],
|
|
87
|
+
],
|
|
88
|
+
chat: [
|
|
89
|
+
"FROM chats n JOIN accounts a ON a.id=n.account_id WHERE n.id=?",
|
|
90
|
+
["n.external_id", "a.external_id", "a.provider"],
|
|
91
|
+
],
|
|
92
|
+
identity: ["FROM identities n WHERE n.id=?", ["n.external_id", "n.provider"]],
|
|
93
|
+
};
|
|
94
|
+
const selection = identifier[type];
|
|
95
|
+
if (selection)
|
|
96
|
+
this.budget.rows(this.database, selection[0], [...selection[1]], [id]);
|
|
97
|
+
const thing = storedThing(type, id), ref = referenceOfThing(this.database, thing);
|
|
98
|
+
if (!ref)
|
|
99
|
+
return invalid();
|
|
100
|
+
this.byThing.set(key, ref);
|
|
101
|
+
this.references.set(ref, thing);
|
|
102
|
+
return ref;
|
|
103
|
+
}
|
|
104
|
+
external(owned) {
|
|
105
|
+
return [...this.references]
|
|
106
|
+
.filter(([ref]) => !owned.has(ref))
|
|
107
|
+
.map(([ref, thing]) => ({
|
|
108
|
+
ref,
|
|
109
|
+
content_sha256: this.fingerprint(thing),
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
fingerprint(thing) {
|
|
113
|
+
this.budget.reserve(1, 128);
|
|
114
|
+
if (noText.has(thing.type))
|
|
115
|
+
return null;
|
|
116
|
+
if (thing.type === "meeting_transcript")
|
|
117
|
+
return this.transcript(thing.id);
|
|
118
|
+
const spec = texts[thing.type];
|
|
119
|
+
if (!spec)
|
|
120
|
+
return invalid();
|
|
121
|
+
const from = `FROM ${spec.table} n ${spec.join ?? ""} WHERE n.id=? AND ${spec.where}`;
|
|
122
|
+
this.budget.rows(this.database, from, spec.fields, [thing.id]);
|
|
123
|
+
const row = this.database
|
|
124
|
+
.prepare(`SELECT ${spec.fields.map((f, i) => `${f} AS v${i}`).join(",")} ${from}`)
|
|
125
|
+
.get(thing.id);
|
|
126
|
+
if (!row)
|
|
127
|
+
return invalid();
|
|
128
|
+
return this.digest(thing.type, spec.fields.map((_f, i) => row[`v${i}`]));
|
|
129
|
+
}
|
|
130
|
+
transcript(id) {
|
|
131
|
+
const from = "FROM meeting_transcripts n JOIN meetings m ON m.id=n.meeting_id WHERE n.id=? AND n.deleted_at IS NULL AND m.deleted_at IS NULL";
|
|
132
|
+
this.budget.rows(this.database, from, ["n.format", "n.language", "n.source"], [id]);
|
|
133
|
+
const header = this.database.prepare(`SELECT n.format,n.language,n.source ${from}`).get(id);
|
|
134
|
+
if (!header)
|
|
135
|
+
return invalid();
|
|
136
|
+
const rows = "FROM meeting_transcript_rows n WHERE n.meeting_transcript_id=?";
|
|
137
|
+
this.budget.rows(this.database, rows, ["n.position", "n.start_ms", "n.end_ms", "n.speaker_name", "n.text"], [id]);
|
|
138
|
+
const cues = this.database
|
|
139
|
+
.prepare(`SELECT n.position,n.start_ms,n.end_ms,n.speaker_name,n.text ${rows} ORDER BY n.position LIMIT 1001`)
|
|
140
|
+
.all(id);
|
|
141
|
+
return this.digest("meeting_transcript", [header, cues]);
|
|
142
|
+
}
|
|
143
|
+
digest(kind, content) {
|
|
144
|
+
return createHash("sha256")
|
|
145
|
+
.update(JSON.stringify(["wirecat.project-source", 1, kind, content]))
|
|
146
|
+
.digest("hex");
|
|
147
|
+
}
|
|
148
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.373.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|