@wirecat/cli-core 0.369.0 → 0.371.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 CHANGED
@@ -7,6 +7,33 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.371.0 — 20.10.2026
11
+
12
+ ### Added
13
+
14
+ - **Project annotation manifest validation from `./store`** checks a bounded, versioned JSON graph of
15
+ project aliases, facts and direct links with explicit external dependencies and omitted facets.
16
+ `decodeProjectAnnotations` validates it, `projectAnnotationsFingerprint` hashes its normalized
17
+ form, and `planProjectAnnotations` checks explicit bindings against a complete target snapshot.
18
+ These helpers do not export a store, restore records or authorize confirmation.
19
+ - **`documentPassage` from `./services`** gives hosts the same bounded paragraph context that project gathering
20
+ uses, so a findings review can show the paragraphs around its cited document without copying that logic.
21
+
22
+ ## 0.370.0 — 20.10.2026
23
+
24
+ ### Changed — may break callers
25
+
26
+ - **`checkGuide` finds a scenario step a shell would change**, so a tool's guide test that passed may now fail. It
27
+ splits each step as a POSIX shell does and reports `>`, `<`, `|`, `&`, `;`, `` ` ``, `$(` and the globs `*`, `?`,
28
+ `[` left outside quotes (`` ` `` and `$(` also inside double quotes), and a quote never closed. `max review
29
+ date>1d` passed before, though a shell writes a file named `1d` and drops the filter. Quote the word:
30
+ `'date>1d'`. `shellProblems(run)` gives the same check for one step.
31
+
32
+ ### Added
33
+
34
+ - **`MEMO_SCENARIOS` holds the titles of memo's cross-tool scenarios** (`personBrief`, `decisionTrail`), so every
35
+ tool's "Works with" list takes them from one place instead of typing its own copy.
36
+
10
37
  ## 0.369.0 — 20.10.2026
11
38
 
12
39
  ### Fixed
@@ -18,6 +18,19 @@ export interface GuideReference {
18
18
  name: string;
19
19
  title: string;
20
20
  }
21
+ /** memo's cross-tool scenarios: the one source of the titles every tool lists, and memo's files must carry. */
22
+ export declare const MEMO_SCENARIOS: {
23
+ readonly personBrief: {
24
+ readonly tool: "memo";
25
+ readonly name: "person-brief";
26
+ readonly title: "One person across every tool";
27
+ };
28
+ readonly decisionTrail: {
29
+ readonly tool: "memo";
30
+ readonly name: "decision-trail";
31
+ readonly title: "What did we decide about X, across every source";
32
+ };
33
+ };
21
34
  export interface GuideOptions {
22
35
  /** Absolute path of the tool's scenario files: `fileURLToPath(new URL("../scenarios", import.meta.url))`. */
23
36
  directory: string;
@@ -40,6 +53,8 @@ export declare const loadScenarios: (directory: string) => Promise<{
40
53
  * program is built. `checkGuide` validates them in the tool's CI.
41
54
  */
42
55
  export declare const guideCommand: (app: Pick<AppIdentity, "command">, options: GuideOptions) => Command;
56
+ /** Characters in a step that a shell would act on: the person who copies it would run something else. */
57
+ export declare const shellProblems: (run: string) => string[];
43
58
  /** What is wrong with one step on this program: the path, each option and its value, the arguments. Never runs it. */
44
59
  export declare const checkStep: (program: Command, run: string) => string[];
45
60
  /**
package/dist/cli/guide.js CHANGED
@@ -7,6 +7,11 @@ import { environmentOf, outputFor } from "./context.js";
7
7
  import { NO_EFFECTS } from "./effects.js";
8
8
  import { itemOf, listOf, renderAnswer } from "./envelope.js";
9
9
  import { PROFILE_FREE } from "./profile.js";
10
+ /** memo's cross-tool scenarios: the one source of the titles every tool lists, and memo's files must carry. */
11
+ export const MEMO_SCENARIOS = {
12
+ personBrief: { tool: "memo", name: "person-brief", title: "One person across every tool" },
13
+ decisionTrail: { tool: "memo", name: "decision-trail", title: "What did we decide about X, across every source" },
14
+ };
10
15
  const FRONT_MATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
11
16
  const FIELDS = new Set(["title", "goal", "steps", "look"]);
12
17
  const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -157,17 +162,49 @@ export const guideCommand = (app, options) => {
157
162
  guides.set(command, options);
158
163
  return command;
159
164
  };
160
- /** A step's words, split as a shell would on spaces, with "…" and '…' kept together. */
161
- const words = (line) => {
165
+ const OPERATORS = {
166
+ ">": "redirects output to a file",
167
+ "<": "reads input from a file",
168
+ "|": "pipes into another command",
169
+ "&": "runs the command in the background",
170
+ ";": "ends the command there",
171
+ "`": "runs it as a command",
172
+ "$(": "runs it as a command",
173
+ "*": "expands it to file names",
174
+ "?": "expands it to file names",
175
+ "[": "expands it to file names",
176
+ };
177
+ /**
178
+ * A step split into words as a POSIX shell would — '…' literal, "…" with \ escapes, \ outside quotes — and the
179
+ * characters a shell would act on instead of passing them to the tool. `` ` `` and `$(` still act inside "…".
180
+ */
181
+ const scan = (line) => {
162
182
  const found = [];
183
+ const live = new Set();
163
184
  let word;
164
185
  let quote;
165
- for (const char of line) {
166
- if (quote) {
167
- if (char === quote)
186
+ for (let index = 0; index < line.length; index++) {
187
+ const char = line[index];
188
+ const next = line[index + 1];
189
+ if (quote === "'") {
190
+ if (char === "'")
168
191
  quote = undefined;
169
192
  else
170
- word = (word ?? "") + char;
193
+ word += char;
194
+ continue;
195
+ }
196
+ if (char === "\\" && next !== undefined && (quote === undefined || '$`"\\'.includes(next))) {
197
+ word = (word ?? "") + next;
198
+ index++;
199
+ continue;
200
+ }
201
+ if (char === "`" || (char === "$" && next === "("))
202
+ live.add(char === "`" ? "`" : "$(");
203
+ if (quote === '"') {
204
+ if (char === '"')
205
+ quote = undefined;
206
+ else
207
+ word += char;
171
208
  }
172
209
  else if (char === '"' || char === "'") {
173
210
  quote = char;
@@ -178,12 +215,24 @@ const words = (line) => {
178
215
  found.push(word);
179
216
  word = undefined;
180
217
  }
181
- else
218
+ else {
219
+ if (OPERATORS[char])
220
+ live.add(char);
182
221
  word = (word ?? "") + char;
222
+ }
183
223
  }
184
224
  if (word !== undefined)
185
225
  found.push(word);
186
- return found;
226
+ return { words: found, live: [...live], open: quote !== undefined };
227
+ };
228
+ const words = (line) => scan(line).words;
229
+ /** Characters in a step that a shell would act on: the person who copies it would run something else. */
230
+ export const shellProblems = (run) => {
231
+ const { live, open } = scan(run);
232
+ return [
233
+ ...live.map((char) => `a shell acts on ${char}: it ${OPERATORS[char]}; put the word in single quotes`),
234
+ ...(open ? ["a quote is never closed"] : []),
235
+ ];
187
236
  };
188
237
  const silent = (command) => command.exitOverride().configureOutput({ writeOut: () => { }, writeErr: () => { }, outputError: () => { } });
189
238
  const ownerOf = (chain, flag) => {
@@ -295,7 +344,7 @@ export const checkGuide = async (build, { packageRoot } = {}) => {
295
344
  const { scenarios, problems } = await loadScenarios(options.directory);
296
345
  for (const scenario of scenarios)
297
346
  scenario.steps.forEach((step, index) => {
298
- for (const problem of checkStep(build(), step.run))
347
+ for (const problem of [...shellProblems(step.run), ...checkStep(build(), step.run)])
299
348
  problems.push(`${scenario.name}.md step ${index + 1} (${step.run}): ${problem}`);
300
349
  });
301
350
  for (const reference of options.worksWith ?? [])
@@ -56,7 +56,7 @@ export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
56
56
  export { changesAuthority, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "./effects.js";
57
57
  export { type AnswerOutput, answerSchema, type Body, type ItemBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, type ListBody, type ListInput, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
58
58
  export { withAbort } from "./execution.js";
59
- export { checkGuide, checkStep, type GuideOptions, type GuideReference, type GuideStep, guideCommand, loadScenarios, type Scenario, } from "./guide.js";
59
+ export { checkGuide, checkStep, type GuideOptions, type GuideReference, type GuideStep, guideCommand, loadScenarios, MEMO_SCENARIOS, type Scenario, shellProblems, } from "./guide.js";
60
60
  export { type CommandHelp, commandHelp, commandHelpMarkdown, commandHelpText, exampleReport, type NextChoice, nextUsually, } from "./help/command-help.js";
61
61
  export { commandsText } from "./help/commands-text.js";
62
62
  export { type FirstScreen, type FirstScreenLine, firstScreen, installHelp, WIRECAT_TOOLS, } from "./help/first-screen.js";
package/dist/cli/index.js CHANGED
@@ -51,7 +51,7 @@ export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
51
51
  export { changesAuthority, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "./effects.js";
52
52
  export { answerSchema, itemAnswer, itemOf, jsonlRecords, judgedAnswer, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
53
53
  export { withAbort } from "./execution.js";
54
- export { checkGuide, checkStep, guideCommand, loadScenarios, } from "./guide.js";
54
+ export { checkGuide, checkStep, guideCommand, loadScenarios, MEMO_SCENARIOS, shellProblems, } from "./guide.js";
55
55
  export { commandHelp, commandHelpMarkdown, commandHelpText, exampleReport, nextUsually, } from "./help/command-help.js";
56
56
  export { commandsText } from "./help/commands-text.js";
57
57
  export { firstScreen, installHelp, WIRECAT_TOOLS, } from "./help/first-screen.js";
@@ -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";
@@ -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";
@@ -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";
@@ -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.369.0",
3
+ "version": "0.371.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",