@wirecat/cli-core 0.370.0 → 0.372.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 +37 -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/services/index.d.ts +1 -1
- package/dist/services/index.js +1 -1
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.js +2 -0
- package/dist/store/project-transfer/manifest.d.ts +6 -0
- package/dist/store/project-transfer/manifest.js +138 -0
- package/dist/store/project-transfer/plan.d.ts +28 -0
- package/dist/store/project-transfer/plan.js +106 -0
- package/dist/store/project-transfer/schema.d.ts +94 -0
- package/dist/store/project-transfer/schema.js +68 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,43 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.372.0 — 20.10.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Setup wizard helpers from `./cli`**: `AGENTS`, `type Agent`, `screenFor(context, quiet, steps)` and
|
|
15
|
+
`agentFor(context, given, pad, prompt?)`, for the `setup` command of a messenger CLI. `screenFor` prints the
|
|
16
|
+
numbered step headings (`[2/5] …`, details indented by 6) to stderr for a person, hides them under `--quiet`,
|
|
17
|
+
and keeps plain notes in a machine mode. `agentFor` asks which agent to install only at a terminal in the pretty
|
|
18
|
+
format and answers `none` otherwise; an unknown answer is a `validation_error`. Both take the minimum shape they
|
|
19
|
+
read (`format`, `streams.diagnostic`, `renderer.note`, `stdin`), so tg-cli's and whatsapp-cli's contexts fit as they
|
|
20
|
+
are. Pass the wizard's step count as `steps`, and the prompt's `input`, `output` and `signal` as `prompt`
|
|
21
|
+
(`secretInputOf(command)` plus the signal, or `{ input: context.stdin }`). No profile or agent is chosen for the
|
|
22
|
+
caller. This replaces the copies in both CLIs' `src/commands/setup.ts`.
|
|
23
|
+
- **`restartServers(app, script, env, spawn?)` from `./cli`** restarts the `serve` of every profile that is running,
|
|
24
|
+
with `<script> <profile> server restart`, and answers `{ restarted, left }`: the shape `upgradeCommand`'s
|
|
25
|
+
`afterUpdate` port returns, so `afterUpdate: () => restartServers(app, script, env, environment.spawn)` is the whole
|
|
26
|
+
host side. `script` is the new install's own entry point; `spawn` replaces the child process in a test. A serve started
|
|
27
|
+
by hand refuses the restart and lands in `left`. This replaces the copies in both CLIs' `src/commands/update.ts` and
|
|
28
|
+
`src/update.ts`.
|
|
29
|
+
- **`ARGV_LOG` and `argvLogSetup` from `./testing`**: the path `coverage/argv.jsonl` under the working directory, and
|
|
30
|
+
a vitest `globalSetup` function that clears it, for the test matrix that checks which commands and options a run
|
|
31
|
+
drove. A CLI's own `src/testing/argv-log.ts` can go. Vitest takes a package specifier as `globalSetup` only with a
|
|
32
|
+
named `setup` or a default export, so keep a one-line file: `export { argvLogSetup as default } from
|
|
33
|
+
"@wirecat/cli-core/testing"`.
|
|
34
|
+
|
|
35
|
+
## 0.371.0 — 20.10.2026
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **Project annotation manifest validation from `./store`** checks a bounded, versioned JSON graph of
|
|
40
|
+
project aliases, facts and direct links with explicit external dependencies and omitted facets.
|
|
41
|
+
`decodeProjectAnnotations` validates it, `projectAnnotationsFingerprint` hashes its normalized
|
|
42
|
+
form, and `planProjectAnnotations` checks explicit bindings against a complete target snapshot.
|
|
43
|
+
These helpers do not export a store, restore records or authorize confirmation.
|
|
44
|
+
- **`documentPassage` from `./services`** gives hosts the same bounded paragraph context that project gathering
|
|
45
|
+
uses, so a findings review can show the paragraphs around its cited document without copying that logic.
|
|
46
|
+
|
|
10
47
|
## 0.370.0 — 20.10.2026
|
|
11
48
|
|
|
12
49
|
### Changed — may break callers
|
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/services/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { type DownloadedModel, downloadImageModel, downloadTextModel, type Model
|
|
|
6
6
|
export type { ProjectEvidenceItem, ProjectGathering, ProjectGatheringPorts, ProjectGatheringStop, ProjectIndexCatchUp, ProjectSourceFetcher, } from "./project-gathering.js";
|
|
7
7
|
export { gatherProjectEvidence } from "./project-gathering.js";
|
|
8
8
|
export type { ProjectNeighbour, ProjectNeighbourReader } from "./project-gathering-around.js";
|
|
9
|
-
export { storedMessageNeighbours, storedNeighbours } from "./project-gathering-around.js";
|
|
9
|
+
export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./project-gathering-around.js";
|
|
10
10
|
export type { EvidenceMention, EvidenceMentions } from "./project-gathering-mentions.js";
|
|
11
11
|
export { findEvidenceMentions } from "./project-gathering-mentions.js";
|
|
12
12
|
export type { ProjectFetchState, ProjectGatheringBudgets, ProjectGatheringRequest, ProjectLane, ProjectLaneKind, ProjectUnitState, } from "./project-gathering-request.js";
|
package/dist/services/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { contextEvidenceFingerprint, createContextEvidenceReader } from "./conte
|
|
|
3
3
|
export { excerpt } from "./excerpt.js";
|
|
4
4
|
export { downloadImageModel, downloadTextModel, } from "./model-downloads.js";
|
|
5
5
|
export { gatherProjectEvidence } from "./project-gathering.js";
|
|
6
|
-
export { storedMessageNeighbours, storedNeighbours } from "./project-gathering-around.js";
|
|
6
|
+
export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./project-gathering-around.js";
|
|
7
7
|
export { findEvidenceMentions } from "./project-gathering-mentions.js";
|
|
8
8
|
import { accountService } from "./account.js";
|
|
9
9
|
import { adminService } from "./admin.js";
|
package/dist/store/index.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export type { MeetingFindingsBatch, MeetingFindingsStore } from "./meeting-findi
|
|
|
14
14
|
export { MIGRATIONS, type Migration, migrate } from "./migrations.js";
|
|
15
15
|
export { openCache } from "./open.js";
|
|
16
16
|
export { storePath } from "./path.js";
|
|
17
|
+
export { decodeProjectAnnotations, PROJECT_ANNOTATIONS_MAX_BYTES, type ProjectAnnotations, projectAnnotationsFingerprint, } from "./project-transfer/manifest.js";
|
|
18
|
+
export { type ProjectAnnotationsPlan, type ProjectAnnotationsTarget, planProjectAnnotations, } from "./project-transfer/plan.js";
|
|
17
19
|
export type { AgentAction, AgentActionInput, AgentActionsStore } from "./sqlite/agent-actions.js";
|
|
18
20
|
export { resetAttachmentWords } from "./sqlite/attachment-texts.js";
|
|
19
21
|
export { backfillNormalized, pendingNormalization } from "./sqlite/backfill.js";
|
package/dist/store/index.js
CHANGED
|
@@ -6,6 +6,8 @@ export { EVIDENCE_FINDINGS_KIND, validateEvidenceFindings, } from "./evidence-fi
|
|
|
6
6
|
export { MIGRATIONS, migrate } from "./migrations.js";
|
|
7
7
|
export { openCache } from "./open.js";
|
|
8
8
|
export { storePath } from "./path.js";
|
|
9
|
+
export { decodeProjectAnnotations, PROJECT_ANNOTATIONS_MAX_BYTES, projectAnnotationsFingerprint, } from "./project-transfer/manifest.js";
|
|
10
|
+
export { planProjectAnnotations, } from "./project-transfer/plan.js";
|
|
9
11
|
export { resetAttachmentWords } from "./sqlite/attachment-texts.js";
|
|
10
12
|
export { backfillNormalized, pendingNormalization } from "./sqlite/backfill.js";
|
|
11
13
|
export { CATALOG_QUERY } from "./sqlite/catalog.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ProjectAnnotations } from "./schema.js";
|
|
2
|
+
export type { ProjectAnnotations } from "./schema.js";
|
|
3
|
+
export declare const PROJECT_ANNOTATIONS_MAX_BYTES: number;
|
|
4
|
+
/** Decode bounded JSON, never a row/column importer or an implicit whole-project backup. */
|
|
5
|
+
export declare const decodeProjectAnnotations: (json: string) => ProjectAnnotations;
|
|
6
|
+
export declare const projectAnnotationsFingerprint: (json: string) => string;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { CliError } from "@wirecat/cli-base";
|
|
3
|
+
import * as v from "valibot";
|
|
4
|
+
import { canonicalReference, parseReference } from "../../domain/references.js";
|
|
5
|
+
import { normalizeVocabularyName } from "../../domain/vocabulary.js";
|
|
6
|
+
import { canonicalMeetingReference } from "../../meetings/index.js";
|
|
7
|
+
import { projectAnnotationsSchema } from "./schema.js";
|
|
8
|
+
export const PROJECT_ANNOTATIONS_MAX_BYTES = 4 * 1024 * 1024;
|
|
9
|
+
const invalid = (field) => {
|
|
10
|
+
throw new CliError("validation_error", `invalid project annotations.${field}`);
|
|
11
|
+
};
|
|
12
|
+
const canonical = (ref) => {
|
|
13
|
+
try {
|
|
14
|
+
return ref.startsWith("meeting:") ? canonicalMeetingReference(ref) : canonicalReference(ref);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return invalid("reference");
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const graph = (manifest) => {
|
|
21
|
+
const owned = new Set([manifest.project.ref]);
|
|
22
|
+
const external = new Set();
|
|
23
|
+
const used = new Set();
|
|
24
|
+
const kind = (ref, kinds) => {
|
|
25
|
+
if (canonical(ref) !== ref || !kinds.includes(parseReference(ref).type) || !/^[a-z]+:[1-9]\d*$/.test(ref))
|
|
26
|
+
invalid("reference kind");
|
|
27
|
+
};
|
|
28
|
+
kind(manifest.project.ref, ["project"]);
|
|
29
|
+
for (const fact of manifest.facts) {
|
|
30
|
+
kind(fact.ref, ["fact"]);
|
|
31
|
+
if (owned.has(fact.ref))
|
|
32
|
+
invalid("facts.duplicate");
|
|
33
|
+
owned.add(fact.ref);
|
|
34
|
+
kind(fact.author, ["person", "bot"]);
|
|
35
|
+
if (fact.confirmed_by !== null)
|
|
36
|
+
kind(fact.confirmed_by, ["person", "bot"]);
|
|
37
|
+
if (fact.updated_at < fact.created_at)
|
|
38
|
+
invalid("facts.updated_at");
|
|
39
|
+
if (fact.type !== "decision" && (fact.supersedes !== null || ["superseded", "reversed"].includes(fact.status)))
|
|
40
|
+
invalid("facts.lifecycle");
|
|
41
|
+
if (fact.supersedes === fact.ref)
|
|
42
|
+
invalid("facts.supersedes");
|
|
43
|
+
}
|
|
44
|
+
for (const item of manifest.external) {
|
|
45
|
+
if (canonical(item.ref) !== item.ref || owned.has(item.ref) || external.has(item.ref))
|
|
46
|
+
invalid("external.ref");
|
|
47
|
+
external.add(item.ref);
|
|
48
|
+
const body = /^(msg|document|note|meeting):/.test(item.ref);
|
|
49
|
+
if (body !== (item.content_sha256 !== null))
|
|
50
|
+
invalid("external.content_sha256");
|
|
51
|
+
}
|
|
52
|
+
const dependency = (ref) => {
|
|
53
|
+
if (ref === null)
|
|
54
|
+
return;
|
|
55
|
+
if (canonical(ref) !== ref || (!owned.has(ref) && !external.has(ref)))
|
|
56
|
+
invalid("missing dependency");
|
|
57
|
+
used.add(ref);
|
|
58
|
+
};
|
|
59
|
+
if (manifest.project.organization !== null)
|
|
60
|
+
kind(manifest.project.organization, ["organization"]);
|
|
61
|
+
dependency(manifest.project.organization);
|
|
62
|
+
const aliases = new Set([manifest.project.name, manifest.project.key].map(normalizeVocabularyName));
|
|
63
|
+
for (const alias of manifest.aliases) {
|
|
64
|
+
const name = normalizeVocabularyName(alias);
|
|
65
|
+
if (aliases.has(name))
|
|
66
|
+
invalid("aliases.duplicate");
|
|
67
|
+
aliases.add(name);
|
|
68
|
+
}
|
|
69
|
+
for (const fact of manifest.facts) {
|
|
70
|
+
for (const ref of [fact.author, fact.confirmed_by, fact.subject, fact.supersedes])
|
|
71
|
+
dependency(ref);
|
|
72
|
+
if (fact.supersedes !== null) {
|
|
73
|
+
kind(fact.supersedes, ["fact"]);
|
|
74
|
+
if (!manifest.facts.some((previous) => previous.ref === fact.supersedes && previous.type === "decision"))
|
|
75
|
+
invalid("facts.supersedes outside project");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const previous = new Map(manifest.facts.map((fact) => [fact.ref, fact.supersedes]));
|
|
79
|
+
for (const fact of manifest.facts) {
|
|
80
|
+
const path = new Set();
|
|
81
|
+
let ref = fact.ref;
|
|
82
|
+
while (ref) {
|
|
83
|
+
if (path.has(ref))
|
|
84
|
+
invalid("facts.supersedes cycle");
|
|
85
|
+
path.add(ref);
|
|
86
|
+
ref = previous.get(ref);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const links = new Set();
|
|
90
|
+
for (const link of manifest.links) {
|
|
91
|
+
if (!/^link:[1-9]\d*$/.test(link.ref) || links.has(link.ref))
|
|
92
|
+
invalid("links.ref");
|
|
93
|
+
links.add(link.ref);
|
|
94
|
+
if (!owned.has(link.from) && (link.to === null || !owned.has(link.to)))
|
|
95
|
+
invalid("links.outside project");
|
|
96
|
+
if (link.to === null && link.target_text === null)
|
|
97
|
+
invalid("links.target_text");
|
|
98
|
+
if (link.updated_at < link.created_at)
|
|
99
|
+
invalid("links.updated_at");
|
|
100
|
+
for (const ref of [link.from, link.to])
|
|
101
|
+
dependency(ref);
|
|
102
|
+
}
|
|
103
|
+
for (const ref of external)
|
|
104
|
+
if (!used.has(ref))
|
|
105
|
+
invalid("external.unused");
|
|
106
|
+
};
|
|
107
|
+
/** Decode bounded JSON, never a row/column importer or an implicit whole-project backup. */
|
|
108
|
+
export const decodeProjectAnnotations = (json) => {
|
|
109
|
+
if (typeof json !== "string" || Buffer.byteLength(json) > PROJECT_ANNOTATIONS_MAX_BYTES)
|
|
110
|
+
invalid("bytes");
|
|
111
|
+
let input;
|
|
112
|
+
try {
|
|
113
|
+
input = JSON.parse(json);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return invalid("json");
|
|
117
|
+
}
|
|
118
|
+
const parsed = v.safeParse(projectAnnotationsSchema, input);
|
|
119
|
+
if (!parsed.success)
|
|
120
|
+
return invalid(parsed.issues[0]?.path?.map((part) => String(part.key)).join(".") ?? "manifest");
|
|
121
|
+
const manifest = parsed.output;
|
|
122
|
+
if (manifest.aliases.length + manifest.facts.length + manifest.links.length + manifest.external.length > 1000)
|
|
123
|
+
invalid("record budget");
|
|
124
|
+
graph(manifest);
|
|
125
|
+
return manifest;
|
|
126
|
+
};
|
|
127
|
+
export const projectAnnotationsFingerprint = (json) => {
|
|
128
|
+
const manifest = decodeProjectAnnotations(json);
|
|
129
|
+
const normalized = {
|
|
130
|
+
...manifest,
|
|
131
|
+
aliases: manifest.aliases.toSorted(),
|
|
132
|
+
facts: manifest.facts.toSorted((a, b) => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0)),
|
|
133
|
+
links: manifest.links.toSorted((a, b) => a.ref.localeCompare(b.ref)),
|
|
134
|
+
external: manifest.external.toSorted((a, b) => a.ref.localeCompare(b.ref)),
|
|
135
|
+
};
|
|
136
|
+
// Strict decoding fixes object key order; record arrays have identity, unlike text and timestamps.
|
|
137
|
+
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
|
|
138
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
declare const targetSchema: v.StrictObjectSchema<{
|
|
3
|
+
readonly project: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>;
|
|
4
|
+
readonly revision_sha256: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>;
|
|
5
|
+
readonly annotation_count: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
6
|
+
readonly external: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
|
|
7
|
+
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 2048, undefined>]>;
|
|
8
|
+
readonly content_sha256: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>;
|
|
9
|
+
}, undefined>, undefined>, v.MaxLengthAction<{
|
|
10
|
+
ref: string;
|
|
11
|
+
content_sha256: string | null;
|
|
12
|
+
}[], 1000, undefined>]>;
|
|
13
|
+
}, undefined>;
|
|
14
|
+
export type ProjectAnnotationsTarget = v.InferOutput<typeof targetSchema>;
|
|
15
|
+
export interface ProjectAnnotationsPlan {
|
|
16
|
+
fingerprint: string;
|
|
17
|
+
manifest_fingerprint: string;
|
|
18
|
+
target_revision_sha256: string;
|
|
19
|
+
missing_bindings: string[];
|
|
20
|
+
conflicts: {
|
|
21
|
+
reference: string;
|
|
22
|
+
reason: "missing" | "wrong_kind" | "changed" | "populated" | "duplicate";
|
|
23
|
+
}[];
|
|
24
|
+
ready: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Pure planning over an adapter's complete target snapshot; a ready plan is not write authorization. */
|
|
27
|
+
export declare const planProjectAnnotations: (json: string, target: ProjectAnnotationsTarget, bindings: Record<string, string>) => ProjectAnnotationsPlan;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { CliError } from "@wirecat/cli-base";
|
|
3
|
+
import * as v from "valibot";
|
|
4
|
+
import { canonicalReference, parseReference } from "../../domain/references.js";
|
|
5
|
+
import { canonicalMeetingReference } from "../../meetings/index.js";
|
|
6
|
+
import { decodeProjectAnnotations, projectAnnotationsFingerprint } from "./manifest.js";
|
|
7
|
+
const hash = v.pipe(v.string(), v.regex(/^[a-f0-9]{64}$/));
|
|
8
|
+
const ref = v.pipe(v.string(), v.minLength(1), v.maxLength(2048));
|
|
9
|
+
const targetSchema = v.strictObject({
|
|
10
|
+
project: v.pipe(v.string(), v.regex(/^project:[1-9]\d*$/)),
|
|
11
|
+
revision_sha256: hash,
|
|
12
|
+
annotation_count: v.pipe(v.number(), v.safeInteger(), v.minValue(0)),
|
|
13
|
+
external: v.pipe(v.array(v.strictObject({ ref, content_sha256: v.nullable(hash) })), v.maxLength(1000)),
|
|
14
|
+
});
|
|
15
|
+
const kind = (ref) => {
|
|
16
|
+
if (ref.startsWith("meeting:")) {
|
|
17
|
+
if (canonicalMeetingReference(ref) !== ref)
|
|
18
|
+
throw new Error("noncanonical");
|
|
19
|
+
return `meeting:${ref.split("/").length}`;
|
|
20
|
+
}
|
|
21
|
+
if (canonicalReference(ref) !== ref)
|
|
22
|
+
throw new Error("noncanonical");
|
|
23
|
+
const parsed = parseReference(ref);
|
|
24
|
+
return "provider" in parsed ? `${parsed.type}:${parsed.provider}` : parsed.type;
|
|
25
|
+
};
|
|
26
|
+
const invalid = (field) => {
|
|
27
|
+
throw new CliError("validation_error", `invalid project annotation plan.${field}`);
|
|
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");
|
|
35
|
+
if (!bindings || typeof bindings !== "object" || Array.isArray(bindings) || Object.keys(bindings).length > 1001)
|
|
36
|
+
return invalid("bindings");
|
|
37
|
+
if (Object.getPrototypeOf(bindings) !== Object.prototype && Object.getPrototypeOf(bindings) !== null)
|
|
38
|
+
return invalid("bindings");
|
|
39
|
+
const required = new Set([manifest.project.ref, ...manifest.external.map((item) => item.ref)]);
|
|
40
|
+
for (const [source, destination] of Object.entries(bindings))
|
|
41
|
+
if (!required.has(source) || typeof destination !== "string" || destination.length > 2048)
|
|
42
|
+
return invalid("bindings");
|
|
43
|
+
const destinations = new Map();
|
|
44
|
+
for (const item of checked.output.external) {
|
|
45
|
+
try {
|
|
46
|
+
kind(item.ref);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return invalid("target.external.ref");
|
|
50
|
+
}
|
|
51
|
+
if (destinations.has(item.ref) || item.ref === target.project)
|
|
52
|
+
return invalid("target.external.duplicate");
|
|
53
|
+
destinations.set(item.ref, item.content_sha256);
|
|
54
|
+
}
|
|
55
|
+
const missing_bindings = [], conflicts = [];
|
|
56
|
+
const used = new Set();
|
|
57
|
+
for (const source of required) {
|
|
58
|
+
const destination = bindings[source];
|
|
59
|
+
if (destination === undefined) {
|
|
60
|
+
missing_bindings.push(source);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
if (kind(source) !== kind(destination)) {
|
|
65
|
+
conflicts.push({ reference: source, reason: "wrong_kind" });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return invalid("bindings.reference");
|
|
71
|
+
}
|
|
72
|
+
if (used.has(destination))
|
|
73
|
+
conflicts.push({ reference: source, reason: "duplicate" });
|
|
74
|
+
used.add(destination);
|
|
75
|
+
if (source === manifest.project.ref) {
|
|
76
|
+
if (destination !== target.project)
|
|
77
|
+
conflicts.push({ reference: source, reason: "missing" });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!destinations.has(destination))
|
|
81
|
+
conflicts.push({ reference: source, reason: "missing" });
|
|
82
|
+
else if (destinations.get(destination) !== manifest.external.find((item) => item.ref === source)?.content_sha256)
|
|
83
|
+
conflicts.push({ reference: source, reason: "changed" });
|
|
84
|
+
}
|
|
85
|
+
if (target.annotation_count > 0)
|
|
86
|
+
conflicts.push({ reference: target.project, reason: "populated" });
|
|
87
|
+
const manifest_fingerprint = projectAnnotationsFingerprint(json);
|
|
88
|
+
const fingerprint = createHash("sha256")
|
|
89
|
+
.update(JSON.stringify({
|
|
90
|
+
manifest_fingerprint,
|
|
91
|
+
target: {
|
|
92
|
+
...checked.output,
|
|
93
|
+
external: checked.output.external.toSorted((a, b) => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0)),
|
|
94
|
+
},
|
|
95
|
+
bindings: Object.fromEntries(Object.entries(bindings).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))),
|
|
96
|
+
}))
|
|
97
|
+
.digest("hex");
|
|
98
|
+
return {
|
|
99
|
+
fingerprint,
|
|
100
|
+
manifest_fingerprint,
|
|
101
|
+
target_revision_sha256: target.revision_sha256,
|
|
102
|
+
missing_bindings,
|
|
103
|
+
conflicts,
|
|
104
|
+
ready: missing_bindings.length === 0 && conflicts.length === 0,
|
|
105
|
+
};
|
|
106
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
export declare const projectAnnotationsSchema: v.StrictObjectSchema<{
|
|
3
|
+
readonly format: v.LiteralSchema<"wirecat.project-annotations", undefined>;
|
|
4
|
+
readonly version: v.LiteralSchema<1, undefined>;
|
|
5
|
+
readonly project: v.StrictObjectSchema<{
|
|
6
|
+
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
7
|
+
readonly key: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>;
|
|
8
|
+
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
9
|
+
readonly type: v.PicklistSchema<["work", "client", "personal", "oss", "other"], undefined>;
|
|
10
|
+
readonly scope: v.PicklistSchema<readonly ["personal", "work"], undefined>;
|
|
11
|
+
readonly organization: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
12
|
+
readonly created_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
13
|
+
}, undefined>;
|
|
14
|
+
readonly coverage: v.StrictObjectSchema<{
|
|
15
|
+
readonly aliases: v.LiteralSchema<"complete", undefined>;
|
|
16
|
+
readonly facts: v.LiteralSchema<"complete", undefined>;
|
|
17
|
+
readonly links: v.LiteralSchema<"complete", undefined>;
|
|
18
|
+
readonly memories: v.LiteralSchema<"omitted", undefined>;
|
|
19
|
+
readonly tasks: v.LiteralSchema<"omitted", undefined>;
|
|
20
|
+
readonly labels: v.LiteralSchema<"omitted", undefined>;
|
|
21
|
+
readonly findings: v.LiteralSchema<"omitted", undefined>;
|
|
22
|
+
}, undefined>;
|
|
23
|
+
readonly aliases: v.SchemaWithPipe<readonly [v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>, v.MaxLengthAction<string[], 1000, undefined>]>;
|
|
24
|
+
readonly facts: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
|
|
25
|
+
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
26
|
+
readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
|
|
27
|
+
readonly statement: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
28
|
+
readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed"], undefined>;
|
|
29
|
+
readonly subject: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
30
|
+
readonly occurred_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
31
|
+
readonly supersedes: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
32
|
+
readonly author: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
33
|
+
readonly model: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
34
|
+
readonly confirmed_by: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
35
|
+
readonly scope: v.NullableSchema<v.PicklistSchema<readonly ["personal", "work"], undefined>, undefined>;
|
|
36
|
+
readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
37
|
+
readonly created_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
38
|
+
readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
39
|
+
}, undefined>, undefined>, v.MaxLengthAction<{
|
|
40
|
+
ref: string;
|
|
41
|
+
type: "decision" | "fact" | "promise" | "question" | "statement";
|
|
42
|
+
statement: string;
|
|
43
|
+
status: "accepted" | "proposed" | "reversed" | "superseded";
|
|
44
|
+
subject: string | null;
|
|
45
|
+
occurred_at: number | null;
|
|
46
|
+
supersedes: string | null;
|
|
47
|
+
author: string;
|
|
48
|
+
model: string | null;
|
|
49
|
+
confirmed_by: string | null;
|
|
50
|
+
scope: "personal" | "work" | null;
|
|
51
|
+
source: string;
|
|
52
|
+
created_at: number;
|
|
53
|
+
updated_at: number;
|
|
54
|
+
}[], 1000, undefined>]>;
|
|
55
|
+
readonly links: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
|
|
56
|
+
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
57
|
+
readonly from: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
58
|
+
readonly to: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
59
|
+
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>;
|
|
60
|
+
readonly origin: v.PicklistSchema<readonly ["file", "owner", "suggested", "message"], undefined>;
|
|
61
|
+
readonly anchor: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
62
|
+
readonly target_text: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
63
|
+
readonly role: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
64
|
+
readonly evidence: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
65
|
+
readonly confirmed: v.BooleanSchema<undefined>;
|
|
66
|
+
readonly provenance: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
67
|
+
readonly created_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
68
|
+
readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
69
|
+
readonly author: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
70
|
+
}, undefined>, undefined>, v.MaxLengthAction<{
|
|
71
|
+
ref: string;
|
|
72
|
+
from: string;
|
|
73
|
+
to: string | null;
|
|
74
|
+
kind: "about" | "answered-by" | "assigned-to" | "blocked-by" | "created-from" | "duplicate-of" | "evidence" | "follow-up-of" | "labelled" | "links-to" | "member-of" | "related-to";
|
|
75
|
+
origin: "file" | "message" | "owner" | "suggested";
|
|
76
|
+
anchor: string | null;
|
|
77
|
+
target_text: string | null;
|
|
78
|
+
role: string | null;
|
|
79
|
+
evidence: string | null;
|
|
80
|
+
confirmed: boolean;
|
|
81
|
+
provenance: string | null;
|
|
82
|
+
created_at: number;
|
|
83
|
+
updated_at: number;
|
|
84
|
+
author: string | null;
|
|
85
|
+
}[], 1000, undefined>]>;
|
|
86
|
+
readonly external: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
|
|
87
|
+
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
88
|
+
readonly content_sha256: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>;
|
|
89
|
+
}, undefined>, undefined>, v.MaxLengthAction<{
|
|
90
|
+
ref: string;
|
|
91
|
+
content_sha256: string | null;
|
|
92
|
+
}[], 1000, undefined>]>;
|
|
93
|
+
}, undefined>;
|
|
94
|
+
export type ProjectAnnotations = v.InferOutput<typeof projectAnnotationsSchema>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
import { FACT_STATUSES, FACT_TYPES, MEMORY_SCOPES } from "../../domain/knowledge.js";
|
|
3
|
+
import { LINK_KINDS } from "../sqlite/link-kinds.js";
|
|
4
|
+
import { LINK_ORIGINS } from "../sqlite/notes.js";
|
|
5
|
+
const text = (max) => v.pipe(v.string(), v.minLength(1), v.maxLength(max));
|
|
6
|
+
const ref = text(2048);
|
|
7
|
+
const timestamp = v.pipe(v.number(), v.safeInteger(), v.minValue(0));
|
|
8
|
+
const nullableRef = v.nullable(ref);
|
|
9
|
+
const bounded = (schema) => v.pipe(v.array(schema), v.maxLength(1000));
|
|
10
|
+
export const projectAnnotationsSchema = v.strictObject({
|
|
11
|
+
format: v.literal("wirecat.project-annotations"),
|
|
12
|
+
version: v.literal(1),
|
|
13
|
+
project: v.strictObject({
|
|
14
|
+
ref,
|
|
15
|
+
key: v.pipe(v.string(), v.regex(/^[A-Z][A-Z0-9]{2,3}$/)),
|
|
16
|
+
name: text(200),
|
|
17
|
+
type: v.picklist(["work", "client", "personal", "oss", "other"]),
|
|
18
|
+
scope: v.picklist(MEMORY_SCOPES),
|
|
19
|
+
organization: nullableRef,
|
|
20
|
+
created_at: timestamp,
|
|
21
|
+
}),
|
|
22
|
+
coverage: v.strictObject({
|
|
23
|
+
aliases: v.literal("complete"),
|
|
24
|
+
facts: v.literal("complete"),
|
|
25
|
+
links: v.literal("complete"),
|
|
26
|
+
memories: v.literal("omitted"),
|
|
27
|
+
tasks: v.literal("omitted"),
|
|
28
|
+
labels: v.literal("omitted"),
|
|
29
|
+
findings: v.literal("omitted"),
|
|
30
|
+
}),
|
|
31
|
+
aliases: bounded(text(200)),
|
|
32
|
+
facts: bounded(v.strictObject({
|
|
33
|
+
ref,
|
|
34
|
+
type: v.picklist(FACT_TYPES),
|
|
35
|
+
statement: text(2000),
|
|
36
|
+
status: v.picklist(FACT_STATUSES),
|
|
37
|
+
subject: nullableRef,
|
|
38
|
+
occurred_at: v.nullable(timestamp),
|
|
39
|
+
supersedes: nullableRef,
|
|
40
|
+
author: ref,
|
|
41
|
+
model: v.nullable(text(200)),
|
|
42
|
+
confirmed_by: nullableRef,
|
|
43
|
+
scope: v.nullable(v.picklist(MEMORY_SCOPES)),
|
|
44
|
+
source: text(200),
|
|
45
|
+
created_at: timestamp,
|
|
46
|
+
updated_at: timestamp,
|
|
47
|
+
})),
|
|
48
|
+
links: bounded(v.strictObject({
|
|
49
|
+
ref: text(200),
|
|
50
|
+
from: ref,
|
|
51
|
+
to: nullableRef,
|
|
52
|
+
kind: v.picklist(LINK_KINDS),
|
|
53
|
+
origin: v.picklist(LINK_ORIGINS),
|
|
54
|
+
anchor: v.nullable(text(2000)),
|
|
55
|
+
target_text: v.nullable(text(2000)),
|
|
56
|
+
role: v.nullable(text(200)),
|
|
57
|
+
evidence: v.nullable(text(2000)),
|
|
58
|
+
confirmed: v.boolean(),
|
|
59
|
+
provenance: v.nullable(text(2000)),
|
|
60
|
+
created_at: timestamp,
|
|
61
|
+
updated_at: timestamp,
|
|
62
|
+
author: v.nullable(text(200)),
|
|
63
|
+
})),
|
|
64
|
+
external: bounded(v.strictObject({
|
|
65
|
+
ref,
|
|
66
|
+
content_sha256: v.nullable(v.pipe(v.string(), v.regex(/^[a-f0-9]{64}$/))),
|
|
67
|
+
})),
|
|
68
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.372.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",
|