@wirecat/cli-core 0.552.0 → 0.553.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.
@@ -49,7 +49,16 @@ export const projectStatusSchema = v.looseObject({
49
49
  attention: v.number(),
50
50
  why: v.nullable(v.string()),
51
51
  })),
52
- out_of_scope: v.optional(v.array(v.strictObject({ chat: v.string(), reason: v.string(), purpose: v.optional(v.string()) }))),
52
+ purposes: v.union([v.literal("all"), v.array(v.string())]),
53
+ out_of_scope: v.optional(v.array(v.strictObject({
54
+ chat: v.string(),
55
+ reason: v.string(),
56
+ purpose: v.optional(v.string()),
57
+ messages: v.optional(v.number()),
58
+ first_at: v.optional(v.nullable(v.string())),
59
+ last_at: v.optional(v.nullable(v.string())),
60
+ read_command: v.optional(v.nullable(v.string())),
61
+ }))),
53
62
  failed: v.optional(v.array(v.strictObject({ signal: v.string(), why: v.string() }))),
54
63
  ...Object.fromEntries(Object.keys(STATUS_SIGNAL_ROWS).map((name) => [listKey(name.slice(7)), v.optional(v.array(statusItem))])),
55
64
  });
@@ -78,9 +87,21 @@ const nextOf = (found, hints) => {
78
87
  const reads = top.flatMap(({ chat, why }) => partial(found, chat) ? [] : because(hints.read(chat, readFrom(found, chat.ref)), why));
79
88
  return [...syncs, ...reads].slice(0, 2);
80
89
  };
81
- const missingOf = (found) => {
90
+ const leftOutOf = (found) => found.outOfScope.filter((one) => one.reason === "purpose");
91
+ const readLine = (found, one, hints) => {
92
+ const step = hints.readChat?.({ ref: one.chat, account: one.account, chatId: one.chatId }, day(found.window.since));
93
+ return step ? hints.line(step) : null;
94
+ };
95
+ /** `12 messages 2026-10-03…2026-11-01`, or that the window holds none. */
96
+ export const heldInWindow = (one) => one.messages && one.firstAt && one.lastAt
97
+ ? `${one.messages} message${one.messages === 1 ? "" : "s"} ${day(one.firstAt)}…${day(one.lastAt)}`
98
+ : "no messages in the window";
99
+ const leftOutWhy = (one, read) => `${one.chat}, purpose ${one.purpose}, not read: ${heldInWindow(one)}` +
100
+ (read ? `; read it: ${read}` : "; add its purpose to --purpose to read it");
101
+ const missingOf = (found, hints) => {
82
102
  const held = found.chats.filter((chat) => partial(found, chat));
83
103
  const [first] = held;
104
+ const others = found.outOfScope.filter((one) => one.reason !== "purpose");
84
105
  return [
85
106
  ...(first
86
107
  ? [
@@ -92,16 +113,20 @@ const missingOf = (found) => {
92
113
  },
93
114
  ]
94
115
  : []),
95
- ...(found.outOfScope.length
116
+ ...(others.length
96
117
  ? [
97
118
  {
98
119
  field: "chats",
99
- why: `${found.outOfScope.length} linked chats were not read here (${[
100
- ...new Set(found.outOfScope.map((one) => one.reason)),
120
+ why: `${others.length} linked chats were not read here (${[
121
+ ...new Set(others.map((one) => one.reason)),
101
122
  ].join(", ")}); memo projects status reads every account`,
102
123
  },
103
124
  ]
104
125
  : []),
126
+ ...leftOutOf(found).map((one) => {
127
+ const read = readLine(found, one, hints);
128
+ return { field: "chats", why: leftOutWhy(one, read), ...(read ? { step: "messages.list" } : {}) };
129
+ }),
105
130
  ...found.failed.map((one) => ({ field: `signal:${one.signal}`, why: one.why })),
106
131
  ];
107
132
  };
@@ -145,11 +170,26 @@ export const projectStatusAnswer = (found, hints) => {
145
170
  attention: weights.get(chat.ref)?.weight ?? 0,
146
171
  why: weights.get(chat.ref)?.why ?? null,
147
172
  })),
148
- ...(found.outOfScope.length ? { outOfScope: found.outOfScope } : {}),
173
+ purposes: found.purposes,
174
+ ...(found.outOfScope.length
175
+ ? {
176
+ outOfScope: found.outOfScope.map((one) => one.reason === "purpose"
177
+ ? {
178
+ chat: one.chat,
179
+ reason: one.reason,
180
+ purpose: one.purpose,
181
+ messages: one.messages,
182
+ firstAt: one.firstAt,
183
+ lastAt: one.lastAt,
184
+ readCommand: readLine(found, one, hints),
185
+ }
186
+ : one),
187
+ }
188
+ : {}),
149
189
  ...(found.failed.length ? { failed: found.failed } : {}),
150
190
  ...lists,
151
191
  });
152
- const missing = missingOf(found);
192
+ const missing = missingOf(found, hints);
153
193
  const complete = found.failed.length === 0 && !found.chats.some((chat) => partial(found, chat));
154
194
  return {
155
195
  item,
@@ -235,8 +275,11 @@ export const projectStatusText = (found, hints) => {
235
275
  const cut = one.data.cut;
236
276
  lines.push(` ${cut ? `${data.child} not read (${cut})` : `${data.key ?? data.child} · ${data.name} · ${data.chats} chats`}`);
237
277
  }
238
- if (found.outOfScope.length)
239
- lines.push(`Not read here: ${found.outOfScope.map((one) => `${one.chat} (${one.reason})`).join(", ")}`);
278
+ const others = found.outOfScope.filter((one) => one.reason !== "purpose");
279
+ if (others.length)
280
+ lines.push(`Not read here: ${others.map((one) => `${one.chat} (${one.reason})`).join(", ")}`);
281
+ for (const one of leftOutOf(found))
282
+ lines.push(`Left out: ${leftOutWhy(one, readLine(found, one, hints))}`);
240
283
  for (const one of found.failed)
241
284
  lines.push(`Signal ${one.signal} failed: ${one.why}`);
242
285
  if (next.length)
@@ -6,7 +6,7 @@ export { type ChatCoverage, chatCoverage } from "./chat-coverage.js";
6
6
  export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextEvidenceSender, ContextIndex, ContextReadLimits, ContextReadUsage, ContextRecordReader, ContextSource, ScopedContextEvidenceReader, } from "./context-evidence.js";
7
7
  export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
8
8
  export { excerpt } from "./excerpt.js";
9
- export { aliasHits, BUILT_IN_PURPOSES, chatSenders, chatTitle, combinedScore, DEFAULT_LIMIT as DEFAULT_LINK_SCAN_LIMIT, DEFAULT_THRESHOLD as DEFAULT_LINK_SCAN_THRESHOLD, emailDomain, type LinkCandidate, type LinkScorer, mailThread, memberChats, memberDialog, PERSON_SCORERS, PERSON_THRESHOLD as PERSON_LINK_SCAN_THRESHOLD, SCAN_BOT, SCORERS, type ScanInput as LinkScanInput, type ScannedProject, type ScannedProposal, type ScanResult as LinkScanResult, type ScorerInput, type Signal as LinkSignal, scanProjectLinks, } from "./link-scorers/index.js";
9
+ export { aliasHits, chatSenders, chatTitle, combinedScore, DEFAULT_LIMIT as DEFAULT_LINK_SCAN_LIMIT, DEFAULT_THRESHOLD as DEFAULT_LINK_SCAN_THRESHOLD, emailDomain, type LinkCandidate, type LinkScorer, mailThread, memberChats, memberDialog, PERSON_SCORERS, PERSON_THRESHOLD as PERSON_LINK_SCAN_THRESHOLD, SCAN_BOT, SCORERS, type ScanInput as LinkScanInput, type ScannedProject, type ScannedProposal, type ScanResult as LinkScanResult, type ScorerInput, type Signal as LinkSignal, scanProjectLinks, } from "./link-scorers/index.js";
10
10
  export type { MediaArchiveOptions, MediaArchivePorts } from "./media-archive.js";
11
11
  export { archiveMedia, checkMediaArchive } from "./media-archive.js";
12
12
  export type { MediaArchiveEntry, MediaIdentity } from "./media-archive-manifest.js";
@@ -21,9 +21,10 @@ export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./pr
21
21
  export type { EvidenceMention, EvidenceMentions } from "./project-gathering-mentions.js";
22
22
  export { findEvidenceMentions } from "./project-gathering-mentions.js";
23
23
  export type { ProjectChatFetched, ProjectGatheringBudgets, ProjectGatheringRequest, ProjectLane, ProjectLaneKind, ProjectLinkedChat, ProjectPerson, ProjectSender, ProjectUnitState, } from "./project-gathering-request.js";
24
- export { DEFAULT_PURPOSES, type MoneyTotal, type ProjectStatus, type ProjectStatusInput, projectStatus, STATUS_CHATS, STATUS_SIGNALS, STATUS_WINDOW, type StatusAttention, type StatusChat, type StatusChatSummary, type StatusContext, type StatusItem, type StatusSignal, } from "./project-status/index.js";
24
+ export { BUILT_IN_PURPOSE_CONFIG, BUILT_IN_PURPOSES, DEFAULT_PURPOSES, LINK_PURPOSE, type PurposeChoice, type PurposeConfig, purposeList, purposeRead, purposesOf, } from "./project-purposes.js";
25
+ export { type MoneyTotal, type OutOfScope, type ProjectStatus, type ProjectStatusInput, projectStatus, STATUS_CHATS, STATUS_SIGNALS, STATUS_WINDOW, type StatusAttention, type StatusChat, type StatusChatSummary, type StatusContext, type StatusItem, type StatusSignal, } from "./project-status/index.js";
25
26
  export { questions } from "./questions.js";
26
- export { canonicalProjectRef, gatherStoredProject, type LinkedChat, linkedChatsOf, type StoredProjectGathering, type StoredProjectInput, } from "./stored-project-gathering.js";
27
+ export { type ChatCount, type ChatCounter, canonicalProjectRef, gatherStoredProject, type LeftOutChat, type LinkedChat, linkedChatsOf, type ProjectMember, type StoredProjectGathering, type StoredProjectInput, storedChatCount, } from "./stored-project-gathering.js";
27
28
  import { type AccountService } from "./account.js";
28
29
  import { type AdminService } from "./admin.js";
29
30
  import { type AdminStatisticsService } from "./admin-statistics.js";
@@ -5,7 +5,7 @@ export { AGENDA_DEFAULTS } from "./agenda.js";
5
5
  export { chatCoverage } from "./chat-coverage.js";
6
6
  export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
7
7
  export { excerpt } from "./excerpt.js";
8
- export { aliasHits, BUILT_IN_PURPOSES, chatSenders, chatTitle, combinedScore, DEFAULT_LIMIT as DEFAULT_LINK_SCAN_LIMIT, DEFAULT_THRESHOLD as DEFAULT_LINK_SCAN_THRESHOLD, emailDomain, mailThread, memberChats, memberDialog, PERSON_SCORERS, PERSON_THRESHOLD as PERSON_LINK_SCAN_THRESHOLD, SCAN_BOT, SCORERS, scanProjectLinks, } from "./link-scorers/index.js";
8
+ export { aliasHits, chatSenders, chatTitle, combinedScore, DEFAULT_LIMIT as DEFAULT_LINK_SCAN_LIMIT, DEFAULT_THRESHOLD as DEFAULT_LINK_SCAN_THRESHOLD, emailDomain, mailThread, memberChats, memberDialog, PERSON_SCORERS, PERSON_THRESHOLD as PERSON_LINK_SCAN_THRESHOLD, SCAN_BOT, SCORERS, scanProjectLinks, } from "./link-scorers/index.js";
9
9
  export { archiveMedia, checkMediaArchive } from "./media-archive.js";
10
10
  export { moveMediaArchive } from "./media-archive-move.js";
11
11
  export { downloadImageModel, downloadTextModel, } from "./model-downloads.js";
@@ -13,9 +13,10 @@ export { MONEY_DONE, MONEY_REQUEST, MONEY_WORDS, moneyClassesOf, PROMISE } from
13
13
  export { gatherProjectEvidence } from "./project-gathering.js";
14
14
  export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./project-gathering-around.js";
15
15
  export { findEvidenceMentions } from "./project-gathering-mentions.js";
16
- export { DEFAULT_PURPOSES, projectStatus, STATUS_CHATS, STATUS_SIGNALS, STATUS_WINDOW, } from "./project-status/index.js";
16
+ export { BUILT_IN_PURPOSE_CONFIG, BUILT_IN_PURPOSES, DEFAULT_PURPOSES, LINK_PURPOSE, purposeList, purposeRead, purposesOf, } from "./project-purposes.js";
17
+ export { projectStatus, STATUS_CHATS, STATUS_SIGNALS, STATUS_WINDOW, } from "./project-status/index.js";
17
18
  export { questions } from "./questions.js";
18
- export { canonicalProjectRef, gatherStoredProject, linkedChatsOf, } from "./stored-project-gathering.js";
19
+ export { canonicalProjectRef, gatherStoredProject, linkedChatsOf, storedChatCount, } from "./stored-project-gathering.js";
19
20
  import { accountService } from "./account.js";
20
21
  import { adminService } from "./admin.js";
21
22
  import { adminStatisticsService } from "./admin-statistics.js";
@@ -5,5 +5,5 @@ export { emailDomain } from "./email-domain.js";
5
5
  export { mailThread } from "./mail-thread.js";
6
6
  export { memberChats } from "./member-chats.js";
7
7
  export { memberDialog } from "./member-dialog.js";
8
- export { BUILT_IN_PURPOSES, combinedScore, DEFAULT_LIMIT, DEFAULT_THRESHOLD, PERSON_SCORERS, PERSON_THRESHOLD, SCAN_BOT, SCORERS, type ScanInput, scanProjectLinks, } from "./scan.js";
8
+ export { combinedScore, DEFAULT_LIMIT, DEFAULT_THRESHOLD, PERSON_SCORERS, PERSON_THRESHOLD, SCAN_BOT, SCORERS, type ScanInput, scanProjectLinks, } from "./scan.js";
9
9
  export type { LinkCandidate, LinkScorer, ScannedProject, ScannedProposal, ScanResult, ScorerInput, Signal, } from "./types.js";
@@ -5,4 +5,4 @@ export { emailDomain } from "./email-domain.js";
5
5
  export { mailThread } from "./mail-thread.js";
6
6
  export { memberChats } from "./member-chats.js";
7
7
  export { memberDialog } from "./member-dialog.js";
8
- export { BUILT_IN_PURPOSES, combinedScore, DEFAULT_LIMIT, DEFAULT_THRESHOLD, PERSON_SCORERS, PERSON_THRESHOLD, SCAN_BOT, SCORERS, scanProjectLinks, } from "./scan.js";
8
+ export { combinedScore, DEFAULT_LIMIT, DEFAULT_THRESHOLD, PERSON_SCORERS, PERSON_THRESHOLD, SCAN_BOT, SCORERS, scanProjectLinks, } from "./scan.js";
@@ -10,7 +10,6 @@ export declare const SCAN_BOT: {
10
10
  readonly bot: "memo-scan";
11
11
  readonly kind: "rule";
12
12
  };
13
- export declare const BUILT_IN_PURPOSES: readonly ["work", "sales", "support", "other"];
14
13
  export declare const DEFAULT_THRESHOLD = 0.5;
15
14
  /** A person ↔ person scan keeps the old rule's reach: one weak signal is enough to ask the owner. */
16
15
  export declare const PERSON_THRESHOLD = 0.3;
@@ -1,5 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { parseReference } from "../../domain/references.js";
3
+ import { BUILT_IN_PURPOSES } from "../project-purposes.js";
3
4
  import { canonicalProjectRef } from "../stored-project-gathering.js";
4
5
  import { aliasHits } from "./alias-hits.js";
5
6
  import { chatSenders } from "./chat-senders.js";
@@ -15,7 +16,6 @@ export const SCORERS = [chatTitle, aliasHits, memberChats, memberDialog, chatSen
15
16
  export const PERSON_SCORERS = [emailDomain];
16
17
  /** The rule bot every scan proposal is written by: only the owner decides its proposals. */
17
18
  export const SCAN_BOT = { bot: "memo-scan", kind: "rule" };
18
- export const BUILT_IN_PURPOSES = ["work", "sales", "support", "other"];
19
19
  export const DEFAULT_THRESHOLD = 0.5;
20
20
  /** A person ↔ person scan keeps the old rule's reach: one weak signal is enough to ask the owner. */
21
21
  export const PERSON_THRESHOLD = 0.3;
@@ -0,0 +1,28 @@
1
+ /** What a chat can be for in a project, when the host's config names none. */
2
+ export declare const BUILT_IN_PURPOSES: readonly string[];
3
+ /** What `projects evidence` and `projects status` read when no purpose is asked for (built in; memo's config may change it). */
4
+ export declare const DEFAULT_PURPOSES: readonly string[];
5
+ /** A link with no purpose is a work chat. */
6
+ export declare const LINK_PURPOSE = "work";
7
+ /** The purposes a host's config lists; core knows only the built-in ones and takes the rest as given. */
8
+ export interface PurposeConfig {
9
+ purposes: readonly string[];
10
+ /** Read when no `--purpose` is given. */
11
+ defaults: readonly string[];
12
+ /** A project's own defaults, by its key or `project:<id>`. */
13
+ projects?: Readonly<Record<string, readonly string[]>>;
14
+ }
15
+ export declare const BUILT_IN_PURPOSE_CONFIG: PurposeConfig;
16
+ /** The purposes one run reads: `all`, or a set. */
17
+ export type PurposeChoice = "all" | ReadonlySet<string>;
18
+ /**
19
+ * The purposes a run reads: the asked ones, each checked against the config's list, else the project's own defaults,
20
+ * else the config's. `all` reads every purpose, a link's unlisted one too.
21
+ */
22
+ export declare const purposesOf: (asked: readonly string[] | undefined, config?: PurposeConfig, project?: {
23
+ ref: string | null;
24
+ key: string | null;
25
+ }) => PurposeChoice;
26
+ export declare const purposeRead: (choice: PurposeChoice, purpose: string | null) => boolean;
27
+ /** `a,b` and repeated flags as one list. */
28
+ export declare const purposeList: (value: string, previous?: string[]) => string[];
@@ -0,0 +1,41 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ /** What a chat can be for in a project, when the host's config names none. */
3
+ export const BUILT_IN_PURPOSES = ["work", "sales", "support", "other"];
4
+ /** What `projects evidence` and `projects status` read when no purpose is asked for (built in; memo's config may change it). */
5
+ export const DEFAULT_PURPOSES = ["work"];
6
+ /** A link with no purpose is a work chat. */
7
+ export const LINK_PURPOSE = "work";
8
+ export const BUILT_IN_PURPOSE_CONFIG = { purposes: BUILT_IN_PURPOSES, defaults: DEFAULT_PURPOSES };
9
+ const unknown = (asked, config) => {
10
+ const wrong = asked.filter((one) => !config.purposes.includes(one));
11
+ if (wrong.length)
12
+ throw new CliError("validation_error", `unknown purpose ${wrong.join(", ")}; the configured ones are ${config.purposes.join(", ")}, or all`, { param: "purpose", purposes: [...config.purposes] });
13
+ };
14
+ /**
15
+ * The purposes a run reads: the asked ones, each checked against the config's list, else the project's own defaults,
16
+ * else the config's. `all` reads every purpose, a link's unlisted one too.
17
+ */
18
+ export const purposesOf = (asked, config = BUILT_IN_PURPOSE_CONFIG, project) => {
19
+ const words = (asked ?? []).map((one) => one.trim()).filter(Boolean);
20
+ if (words.includes("all")) {
21
+ if (words.length > 1)
22
+ throw new CliError("validation_error", "--purpose all reads every purpose; give it alone");
23
+ return "all";
24
+ }
25
+ if (words.length) {
26
+ unknown(words, config);
27
+ return new Set(words);
28
+ }
29
+ const own = (project?.key ? config.projects?.[project.key] : undefined) ??
30
+ (project?.ref ? config.projects?.[project.ref] : undefined);
31
+ return new Set(own ?? config.defaults);
32
+ };
33
+ export const purposeRead = (choice, purpose) => choice === "all" || choice.has(purpose ?? LINK_PURPOSE);
34
+ /** `a,b` and repeated flags as one list. */
35
+ export const purposeList = (value, previous = []) => [
36
+ ...previous,
37
+ ...value
38
+ .split(",")
39
+ .map((one) => one.trim())
40
+ .filter(Boolean),
41
+ ];
@@ -1,4 +1,7 @@
1
+ import type { Id } from "../../domain/models.js";
1
2
  import type { AccountKey, MessageStore } from "../../store/store.js";
3
+ import { type PurposeConfig } from "../project-purposes.js";
4
+ import { type ChatCount, type ChatCounter } from "../stored-project-gathering.js";
2
5
  import { STATUS_SIGNALS } from "./signals/index.js";
3
6
  import type { StatusAttention, StatusChat, StatusItem, StatusProject, StatusSignal } from "./types.js";
4
7
  export type * from "./types.js";
@@ -7,8 +10,6 @@ export { STATUS_SIGNALS };
7
10
  export declare const STATUS_WINDOW = 300;
8
11
  /** Linked chats read at most, as `projects evidence` reads. */
9
12
  export declare const STATUS_CHATS = 16;
10
- /** What `projects status` reads when no purpose is asked for (built in; memo's config may change it). */
11
- export declare const DEFAULT_PURPOSES: readonly string[];
12
13
  export interface ProjectStatusInput {
13
14
  /** An id, key, name or alias, as `canonicalProjectRef` takes it. */
14
15
  project: string;
@@ -18,12 +19,27 @@ export interface ProjectStatusInput {
18
19
  after?: number;
19
20
  asOf?: number;
20
21
  contextQueries?: string[];
21
- /** The chat purposes read; `all` reads every one. Default `DEFAULT_PURPOSES`. */
22
+ /** The chat purposes read; `["all"]` reads every one. Left out, the config's defaults. */
22
23
  purposes?: readonly string[];
24
+ /** The host's purposes; core's built-in ones when left out. */
25
+ purposeConfig?: PurposeConfig;
26
+ /** How many messages a chat left out by purpose holds in the window; the store's own count when left out. */
27
+ count?: ChatCounter;
23
28
  /** Default `STATUS_SIGNALS`; tests pass their own. */
24
29
  signals?: readonly StatusSignal[];
25
30
  }
26
31
  export type StatusChatSummary = Omit<StatusChat, "messages" | "account">;
32
+ /** A linked chat not read; one left out by its purpose says how much it holds in the window. */
33
+ export type OutOfScope = {
34
+ chat: string;
35
+ reason: "other-account" | "linked-chat-limit";
36
+ } | ({
37
+ chat: string;
38
+ reason: "purpose";
39
+ purpose: string;
40
+ account: AccountKey;
41
+ chatId: Id;
42
+ } & ChatCount);
27
43
  export interface MoneyTotal {
28
44
  direction: string;
29
45
  currency: string | null;
@@ -48,11 +64,9 @@ export interface ProjectStatus {
48
64
  money: {
49
65
  open: MoneyTotal[];
50
66
  };
51
- outOfScope: {
52
- chat: string;
53
- reason: "other-account" | "linked-chat-limit" | "purpose";
54
- purpose?: string;
55
- }[];
67
+ outOfScope: OutOfScope[];
68
+ /** The purposes read: `all`, or the list. */
69
+ purposes: "all" | string[];
56
70
  failed: {
57
71
  signal: string;
58
72
  why: string;
@@ -1,7 +1,8 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { formatReference } from "../../domain/references.js";
3
3
  import { chatCoverage } from "../chat-coverage.js";
4
- import { canonicalProjectRef, linkedChatsOf } from "../stored-project-gathering.js";
4
+ import { LINK_PURPOSE, purposeRead, purposesOf } from "../project-purposes.js";
5
+ import { canonicalProjectRef, linkedChatsOf, storedChatCount, } from "../stored-project-gathering.js";
5
6
  import { centsText } from "./signals/amounts.js";
6
7
  import { STATUS_SIGNALS } from "./signals/index.js";
7
8
  export { STATUS_SIGNALS };
@@ -12,8 +13,6 @@ export const STATUS_WINDOW = 300;
12
13
  export const STATUS_CHATS = 16;
13
14
  const CHILD_DEPTH = 2;
14
15
  const CHILDREN = 8;
15
- /** What `projects status` reads when no purpose is asked for (built in; memo's config may change it). */
16
- export const DEFAULT_PURPOSES = ["work"];
17
16
  const projectsCatalog = async (store) => new Map((await store.knowledge.vocabulary({ kind: "project" })).entries.map((entry) => [`project:${entry.id}`, entry]));
18
17
  /** Confirmed `member-of` links from another project; depth two, eight at most, a project seen twice skipped. */
19
18
  const childrenOf = async (store, root, catalog) => {
@@ -92,13 +91,14 @@ export const projectStatus = async (store, input) => {
92
91
  if (!(since < now))
93
92
  throw new CliError("validation_error", "the window must start before it ends");
94
93
  const signals = input.signals ?? STATUS_SIGNALS;
95
- const purposes = new Set(input.purposes ?? DEFAULT_PURPOSES);
96
94
  const catalog = await projectsCatalog(store);
97
95
  const ref = await canonicalProjectRef(store, input.project);
98
96
  const entry = catalog.get(ref);
99
97
  if (!entry)
100
98
  throw new CliError("not_found", "the owner project is unavailable");
101
99
  const root = { ref, key: entry.key, name: entry.name };
100
+ const purposes = purposesOf(input.purposes, input.purposeConfig, root);
101
+ const count = input.count ?? storedChatCount(store);
102
102
  const { children, cut } = await childrenOf(store, root, catalog);
103
103
  const outOfScope = [];
104
104
  const linked = new Map();
@@ -110,10 +110,17 @@ export const projectStatus = async (store, input) => {
110
110
  }
111
111
  const chosen = [];
112
112
  for (const [chat, link] of linked) {
113
- // Links carry no purpose yet; every one is `work`, as a link without one will be.
114
- const purpose = "work";
115
- if (!purposes.has("all") && !purposes.has(purpose))
116
- outOfScope.push({ chat, reason: "purpose", purpose });
113
+ if (!purposeRead(purposes, link.purpose)) {
114
+ const account = { provider: link.provider, account: link.account };
115
+ outOfScope.push({
116
+ chat,
117
+ reason: "purpose",
118
+ purpose: link.purpose ?? LINK_PURPOSE,
119
+ account,
120
+ chatId: link.chat,
121
+ ...(await count(account, link.chat, { since, until: now })),
122
+ });
123
+ }
117
124
  else if (!inAccounts(input.accounts, link.provider, link.account))
118
125
  outOfScope.push({ chat, reason: "other-account" });
119
126
  else if (chosen.length >= STATUS_CHATS)
@@ -134,7 +141,7 @@ export const projectStatus = async (store, input) => {
134
141
  title: coverage?.title ?? null,
135
142
  kind: (await store.chats.chatKind(account, link.chat)) ?? "unknown",
136
143
  role: link.role,
137
- purpose: "work",
144
+ purpose: link.purpose ?? LINK_PURPOSE,
138
145
  project: link.project,
139
146
  coverage: coverage,
140
147
  messages: page.items.filter((message) => Date.parse(message.timestamp) <= now),
@@ -189,6 +196,7 @@ export const projectStatus = async (store, input) => {
189
196
  items,
190
197
  money: { open: totalsOf(items) },
191
198
  outOfScope,
199
+ purposes: purposes === "all" ? "all" : [...purposes],
192
200
  failed,
193
201
  attention,
194
202
  signals: signals.map(({ id, version }) => ({ id, version })),
@@ -1,7 +1,8 @@
1
1
  import type { ContextSource } from "../context/evidence.js";
2
- import type { MessageStore } from "../store/store.js";
2
+ import type { AccountKey, MessageStore } from "../store/store.js";
3
3
  import { gatherProjectEvidence, type ProjectChatFetcher, type ProjectGatheringPorts } from "./project-gathering.js";
4
4
  import { type EvidenceMentions } from "./project-gathering-mentions.js";
5
+ import { type PurposeConfig } from "./project-purposes.js";
5
6
  export interface StoredProjectInput {
6
7
  project: string;
7
8
  /** The sources the host resolved from its `--source`, and those it could not read. */
@@ -31,19 +32,81 @@ export interface StoredProjectInput {
31
32
  maxFetchMs?: number;
32
33
  onFetchProgress?: ProjectGatheringPorts["onFetchProgress"];
33
34
  signal?: AbortSignal;
35
+ /** The linked chats' purposes to read; `["all"]` reads every one. Left out, the config's defaults. */
36
+ purposes?: readonly string[];
37
+ /** The host's purposes; core's built-in ones when left out. */
38
+ purposeConfig?: PurposeConfig;
39
+ /** Proposed `member-of` links count as linked chats and members too, after the confirmed ones, every item marked. */
40
+ includeProposed?: boolean;
41
+ /** How many messages a left-out chat holds in the window; the store's own count when left out. */
42
+ count?: ChatCounter;
34
43
  }
44
+ export interface ChatCount {
45
+ messages: number;
46
+ firstAt: string | null;
47
+ lastAt: string | null;
48
+ }
49
+ export type ChatCounter = (account: AccountKey, chatId: string, window: {
50
+ since: number;
51
+ until: number;
52
+ }) => Promise<ChatCount>;
53
+ /** The store's count of one chat's messages in a window, `until` exclusive, and the first and last of them. */
54
+ export declare const storedChatCount: (store: MessageStore) => ChatCounter;
35
55
  /** `project:<id>`, or the one project a key, name or alias names; several or none is refused, never guessed. */
36
56
  export declare const canonicalProjectRef: (store: MessageStore, reference: string) => Promise<string>;
57
+ export interface ProjectMember {
58
+ person: string;
59
+ state: "read" | "member-limit" | "no-identity";
60
+ /** The proposal that names this person a member; read only with `includeProposed`. */
61
+ proposed?: string;
62
+ score?: number;
63
+ }
37
64
  export interface LinkedChat {
38
65
  provider: string;
39
66
  account: string;
40
67
  chat: string;
41
68
  /** The link's free role (`source report`, `what worked`), if the owner set one. */
42
69
  role: string | null;
70
+ /** What the chat is for in the project; `null` means `work`. */
71
+ purpose: string | null;
72
+ /** The confirmed link's id; null for a proposed one. */
73
+ relationship: string | null;
74
+ /** The proposal that links the chat; only with `includeProposed`. */
75
+ proposed?: string;
76
+ score?: number;
77
+ }
78
+ /**
79
+ * Links the owner confirmed, then, when asked, the proposed ones: a suggestion would put a whole chat into the
80
+ * evidence on a guess, so it is read only on request and marked.
81
+ */
82
+ export declare const linkedChatsOf: (store: MessageStore, project: string | null, options?: {
83
+ includeProposed?: boolean;
84
+ }) => Promise<LinkedChat[]>;
85
+ /** A linked chat whose purpose this run did not read: counted, never listed. */
86
+ export interface LeftOutChat {
87
+ ref: string;
88
+ provider: string;
89
+ account: string;
90
+ chat: string;
91
+ title: string | null;
92
+ purpose: string;
93
+ relationship: string | null;
94
+ proposed?: string;
95
+ messages: number;
96
+ firstAt: string | null;
97
+ lastAt: string | null;
43
98
  }
44
- /** Only links the owner confirmed: a suggested one would put a whole chat into the evidence on a guess. */
45
- export declare const linkedChatsOf: (store: MessageStore, project: string | null) => Promise<LinkedChat[]>;
46
- export type StoredProjectGathering = Awaited<ReturnType<typeof gatherProjectEvidence>> & {
99
+ type Gathered = Awaited<ReturnType<typeof gatherProjectEvidence>>;
100
+ export type StoredProjectGathering = Omit<Gathered, "items" | "linkedChats"> & {
101
+ /** `proposed`: the proposal whose link alone brought the item in; only with `includeProposed`. */
102
+ items: (Gathered["items"][number] & {
103
+ proposed?: string;
104
+ })[];
105
+ linkedChats: (Gathered["linkedChats"][number] & {
106
+ purpose: string;
107
+ proposed?: string;
108
+ score?: number;
109
+ })[];
47
110
  projectRef: string | null;
48
111
  unreadSources: {
49
112
  source: string;
@@ -51,9 +114,21 @@ export type StoredProjectGathering = Awaited<ReturnType<typeof gatherProjectEvid
51
114
  reason?: "out-of-scope" | "linked-chat-limit" | "member-limit" | "no-identity" | "unknown-provider";
52
115
  }[];
53
116
  mentions: EvidenceMentions;
117
+ /** The purposes read: `all`, or the list. */
118
+ purposes: "all" | string[];
119
+ leftOut: LeftOutChat[];
120
+ members: ProjectMember[];
121
+ /** One linked chat with no purpose set gave most of this run's items; a first run only, so it is said once. */
122
+ purposeHint: {
123
+ chat: string;
124
+ relationship: string;
125
+ items: number;
126
+ of: number;
127
+ } | null;
54
128
  };
55
129
  /**
56
130
  * `projects evidence` over a whole store: the project's names, key and aliases, the given words and contacts, its
57
131
  * confirmed person members and its confirmed linked chats, in the sources the host resolved.
58
132
  */
59
133
  export declare const gatherStoredProject: (store: MessageStore, input: StoredProjectInput) => Promise<StoredProjectGathering>;
134
+ export {};