@wirecat/cli-core 0.362.0 → 0.364.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 +27 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/meeting-findings-input.d.ts +2 -1
- package/dist/cli/meeting-findings-input.js +10 -7
- package/dist/cli/messenger/context.d.ts +2 -2
- package/dist/cli/messenger/context.js +6 -2
- package/dist/cli/messenger/flooded.d.ts +2 -1
- package/dist/cli/messenger/flooded.js +32 -12
- package/dist/cli/messenger/replies-command.js +7 -5
- package/dist/cli/messenger/replies-edit-command.js +3 -2
- package/dist/cli/messenger/serve-command.js +3 -2
- package/dist/cli/messenger/serve-relay.d.ts +1 -1
- package/dist/cli/messenger/stored.js +5 -1
- package/dist/context/evidence.d.ts +6 -0
- package/dist/context/evidence.js +4 -0
- package/dist/errors/reasons.js +2 -0
- package/dist/meetings/findings-rules.d.ts +60 -0
- package/dist/meetings/findings-rules.js +117 -0
- package/dist/meetings/findings.d.ts +10 -48
- package/dist/meetings/findings.js +10 -116
- package/dist/meetings/index.d.ts +1 -0
- package/dist/meetings/index.js +1 -0
- package/dist/replies/rendering.d.ts +2 -1
- package/dist/replies/rendering.js +2 -2
- package/dist/replies/rules.d.ts +6 -1
- package/dist/replies/rules.js +12 -4
- package/dist/replies/serve.d.ts +2 -1
- package/dist/replies/serve.js +2 -2
- package/dist/replies/template.d.ts +3 -0
- package/dist/replies/template.js +14 -2
- package/dist/sends/guard.d.ts +3 -2
- package/dist/sends/guard.js +5 -1
- package/dist/services/context-evidence.d.ts +2 -1
- package/dist/services/context-evidence.js +1 -0
- package/dist/services/index.d.ts +3 -2
- package/dist/services/index.js +2 -1
- package/dist/services/project-gathering.d.ts +1 -0
- package/dist/services/project-gathering.js +3 -0
- package/dist/services/proposal-tasks.js +5 -0
- package/dist/store/evidence-findings.d.ts +63 -0
- package/dist/store/evidence-findings.js +15 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.js +1 -0
- package/dist/store/sqlite/context-evidence.d.ts +9 -1
- package/dist/store/sqlite/context-evidence.js +43 -1
- package/dist/store/sqlite/evidence-findings.d.ts +3 -0
- package/dist/store/sqlite/evidence-findings.js +172 -0
- package/dist/store/sqlite/findings-core.d.ts +17 -0
- package/dist/store/sqlite/findings-core.js +108 -0
- package/dist/store/sqlite/meeting-findings.js +47 -121
- package/dist/store/sqlite/proposed-actions.d.ts +4 -1
- package/dist/store/sqlite/proposed-actions.js +12 -4
- package/dist/store/store.d.ts +5 -1
- package/dist/store/store.js +10 -1
- package/package.json +1 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { contextEvidenceFingerprint } from "../../context/evidence.js";
|
|
3
|
+
import { findingFingerprint, findingHash } from "../../meetings/index.js";
|
|
4
|
+
import { EVIDENCE_FINDINGS_KIND, validateEvidenceFindings, } from "../evidence-findings.js";
|
|
5
|
+
import { contextRecordOf } from "./context-evidence.js";
|
|
6
|
+
import { findingsAgent, findingsCoreOver } from "./findings-core.js";
|
|
7
|
+
import { authorActor } from "./memories.js";
|
|
8
|
+
import { proposalOperationsOver } from "./proposed-actions.js";
|
|
9
|
+
import { write } from "./transaction.js";
|
|
10
|
+
const stale = () => {
|
|
11
|
+
throw new CliError("validation_error", "evidence findings changed since they were read; stage a new batch", {
|
|
12
|
+
reason: "evidence-findings.stale",
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
const invalid = (message) => {
|
|
16
|
+
throw new CliError("validation_error", message);
|
|
17
|
+
};
|
|
18
|
+
const TEXT_BUDGET = 262144;
|
|
19
|
+
export const evidenceFindingsStoreOver = (context) => {
|
|
20
|
+
const { database } = context;
|
|
21
|
+
const proposals = proposalOperationsOver(context);
|
|
22
|
+
const core = findingsCoreOver(context, EVIDENCE_FINDINGS_KIND);
|
|
23
|
+
const load = (id) => {
|
|
24
|
+
if (typeof id !== "string" || !/^[1-9]\d*$/.test(id) || !Number.isSafeInteger(Number(id)))
|
|
25
|
+
return invalid("a findings batch needs a stored ID");
|
|
26
|
+
const row = database
|
|
27
|
+
.prepare("SELECT length(CAST(payload AS BLOB)) AS bytes,length(CAST(result AS BLOB)) AS result_bytes FROM proposed_actions WHERE id=? AND account_id IS NULL AND kind=?")
|
|
28
|
+
.get(Number(id), EVIDENCE_FINDINGS_KIND);
|
|
29
|
+
if (!row)
|
|
30
|
+
throw new CliError("not_found", "the evidence findings batch is unavailable");
|
|
31
|
+
if (Number(row.bytes) > 100000 || Number(row.result_bytes) > 65536)
|
|
32
|
+
return invalid("stored findings exceed the read budget");
|
|
33
|
+
return proposals.get(id);
|
|
34
|
+
};
|
|
35
|
+
const view = (proposal) => {
|
|
36
|
+
const input = proposal.payload;
|
|
37
|
+
if (!input || !proposal.target || !["proposed", "approved"].includes(proposal.status))
|
|
38
|
+
return invalid("invalid stored findings batch");
|
|
39
|
+
return {
|
|
40
|
+
id: proposal.id,
|
|
41
|
+
project: proposal.target,
|
|
42
|
+
input: { schemaVersion: 1, key: input.key, extractor: input.extractor, findings: input.findings },
|
|
43
|
+
fingerprint: findingHash({ input, author: proposal.proposedBy.id }),
|
|
44
|
+
status: proposal.status === "proposed" ? "proposed" : "reviewed",
|
|
45
|
+
author: proposal.proposedBy,
|
|
46
|
+
decidedBy: proposal.decidedBy,
|
|
47
|
+
decidedAt: proposal.decidedAt,
|
|
48
|
+
previous: proposal.result
|
|
49
|
+
? proposal.result.items
|
|
50
|
+
: input.findings.flatMap((finding) => {
|
|
51
|
+
const held = core.previous(null, findingFingerprint(finding));
|
|
52
|
+
return held ? [{ ...held, id: finding.id, reused: true }] : [];
|
|
53
|
+
}),
|
|
54
|
+
receipt: proposal.result,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
const snapshot = (input) => {
|
|
58
|
+
let readBytes = 0;
|
|
59
|
+
return core.snapshot(input, (evidence, _finding, _project, seen) => {
|
|
60
|
+
if (seen.has(evidence.reference)) {
|
|
61
|
+
if (seen.get(evidence.reference) !== evidence.fingerprint)
|
|
62
|
+
return stale();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const record = contextRecordOf(database, evidence.reference);
|
|
66
|
+
if (!record)
|
|
67
|
+
return stale();
|
|
68
|
+
readBytes += Buffer.byteLength(record.text) + Buffer.byteLength(record.title ?? "");
|
|
69
|
+
if (readBytes > TEXT_BUDGET)
|
|
70
|
+
return invalid(`finding evidence exceeds the ${TEXT_BUDGET}-byte stored text budget`);
|
|
71
|
+
const fingerprint = contextEvidenceFingerprint(record);
|
|
72
|
+
if (fingerprint !== evidence.fingerprint)
|
|
73
|
+
return stale();
|
|
74
|
+
seen.set(evidence.reference, fingerprint);
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const decider = (by) => {
|
|
78
|
+
if (by === undefined || by === "owner")
|
|
79
|
+
return { author: "owner", decision: { by: "owner" } };
|
|
80
|
+
const raw = by;
|
|
81
|
+
const agent = findingsAgent({
|
|
82
|
+
bot: raw.bot,
|
|
83
|
+
...(raw.model === undefined ? {} : { model: raw.model }),
|
|
84
|
+
});
|
|
85
|
+
if (typeof raw.reason !== "string" || !raw.reason.trim() || raw.reason.length > 2000)
|
|
86
|
+
return invalid("an agent deciding for the owner gives a reason of 1–2000 characters");
|
|
87
|
+
return {
|
|
88
|
+
author: agent,
|
|
89
|
+
decision: { by: "agent", bot: agent.bot, model: agent.model ?? null, reason: raw.reason.trim() },
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
stage: async (raw, rawAgent) => {
|
|
94
|
+
const input = validateEvidenceFindings(raw);
|
|
95
|
+
const projects = new Set(input.findings.map((finding) => finding.project));
|
|
96
|
+
if (projects.size !== 1)
|
|
97
|
+
return invalid("an evidence findings batch is about one project");
|
|
98
|
+
const by = findingsAgent(rawAgent);
|
|
99
|
+
return write(database, () => {
|
|
100
|
+
const { actor } = authorActor(context, by);
|
|
101
|
+
const inputHash = findingHash({ input, model: by.model ?? null });
|
|
102
|
+
const held = database
|
|
103
|
+
.prepare("SELECT id FROM proposed_actions WHERE account_id IS NULL AND kind=? AND proposed_by_type=? AND proposed_by_id=? AND json_extract(payload,'$.key')=? ORDER BY id LIMIT 1")
|
|
104
|
+
.get(EVIDENCE_FINDINGS_KIND, actor.type, actor.id, input.key);
|
|
105
|
+
if (held) {
|
|
106
|
+
const proposal = load(String(held.id));
|
|
107
|
+
if (proposal.payload.inputHash !== inputHash)
|
|
108
|
+
return invalid("the findings batch key was already used for different input");
|
|
109
|
+
return view(proposal);
|
|
110
|
+
}
|
|
111
|
+
const snapshotHash = snapshot(input);
|
|
112
|
+
const fingerprints = input.findings.map(findingFingerprint);
|
|
113
|
+
if (new Set(fingerprints).size !== fingerprints.length)
|
|
114
|
+
return invalid("the batch contains duplicate findings");
|
|
115
|
+
return view(proposals.propose({
|
|
116
|
+
kind: EVIDENCE_FINDINGS_KIND,
|
|
117
|
+
by,
|
|
118
|
+
target: [...projects][0],
|
|
119
|
+
payload: { ...input, inputHash, snapshotHash, agentModel: by.model ?? null },
|
|
120
|
+
}));
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
get: async (id) => view(load(id)),
|
|
124
|
+
list: async ({ project, limit = 20, offset = 0, status } = {}) => {
|
|
125
|
+
if (status !== undefined && status !== "proposed" && status !== "reviewed")
|
|
126
|
+
return invalid("finding status is proposed or reviewed");
|
|
127
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 50 || !Number.isSafeInteger(offset) || offset < 0)
|
|
128
|
+
return invalid("findings lists take 1–50 items and a nonnegative offset");
|
|
129
|
+
const projectId = project === undefined ? null : /^project:([1-9]\d*)$/.exec(project)?.[1];
|
|
130
|
+
if (projectId === undefined)
|
|
131
|
+
return invalid("a findings list filters by a project reference");
|
|
132
|
+
const ids = database
|
|
133
|
+
.prepare("SELECT id FROM proposed_actions WHERE account_id IS NULL AND kind=? AND (? IS NULL OR (target_type='project' AND target_id=?)) AND (? IS NULL OR status=?) ORDER BY id DESC LIMIT ? OFFSET ?")
|
|
134
|
+
.all(EVIDENCE_FINDINGS_KIND, projectId, projectId === null ? null : Number(projectId), status ?? null, status === "reviewed" ? "approved" : (status ?? null), limit + 1, offset);
|
|
135
|
+
return { items: ids.slice(0, limit).map((row) => view(load(String(row.id)))), hasMore: ids.length > limit };
|
|
136
|
+
},
|
|
137
|
+
review: async (id, decision) => write(database, () => {
|
|
138
|
+
const proposal = load(id);
|
|
139
|
+
const batch = view(proposal);
|
|
140
|
+
if (!decision || typeof decision.fingerprint !== "string" || decision.fingerprint !== batch.fingerprint)
|
|
141
|
+
return stale();
|
|
142
|
+
if (!Array.isArray(decision.accept) ||
|
|
143
|
+
decision.accept.length > 16 ||
|
|
144
|
+
new Set(decision.accept).size !== decision.accept.length ||
|
|
145
|
+
decision.accept.some((candidate) => !batch.input.findings.some((finding) => finding.id === candidate)))
|
|
146
|
+
return invalid("accept only unique IDs shown in this batch");
|
|
147
|
+
const accepted = [...decision.accept].sort();
|
|
148
|
+
const { author, decision: decidedBy } = decider(decision.by);
|
|
149
|
+
if (batch.receipt) {
|
|
150
|
+
if (JSON.stringify(batch.receipt.accepted) !== JSON.stringify(accepted))
|
|
151
|
+
return invalid("this findings batch was already reviewed with a different decision");
|
|
152
|
+
return batch;
|
|
153
|
+
}
|
|
154
|
+
const input = proposal.payload;
|
|
155
|
+
if (snapshot(batch.input) !== input.snapshotHash)
|
|
156
|
+
return stale();
|
|
157
|
+
if (proposal.proposedBy.type !== "bot" || !proposal.proposedBy.name)
|
|
158
|
+
return invalid("a findings author must be an agent");
|
|
159
|
+
const agent = {
|
|
160
|
+
bot: proposal.proposedBy.name,
|
|
161
|
+
...(input.agentModel === null ? {} : { model: input.agentModel }),
|
|
162
|
+
};
|
|
163
|
+
const items = core.outcomes(null, batch.input, accepted, agent);
|
|
164
|
+
return view(proposals.approve(id, {
|
|
165
|
+
previewFingerprint: batch.fingerprint,
|
|
166
|
+
accepted,
|
|
167
|
+
items,
|
|
168
|
+
decision: decidedBy,
|
|
169
|
+
}, author));
|
|
170
|
+
}),
|
|
171
|
+
};
|
|
172
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Finding, type FindingEvidence, type FindingOutcome, type FindingsInput } from "../../meetings/index.js";
|
|
2
|
+
import type { StoreContext } from "./open.js";
|
|
3
|
+
export interface FindingsAgent {
|
|
4
|
+
bot: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const findingsAgent: (agent: FindingsAgent) => FindingsAgent;
|
|
8
|
+
/**
|
|
9
|
+
* What meeting and evidence findings share: the project and assignee snapshot, earlier decisions on an unchanged
|
|
10
|
+
* candidate, creating the accepted facts and tasks, and the receipt items. Each kind checks its own evidence,
|
|
11
|
+
* including a reference it already saw for another finding.
|
|
12
|
+
*/
|
|
13
|
+
export declare const findingsCoreOver: (context: StoreContext, kind: string) => {
|
|
14
|
+
snapshot: (input: FindingsInput, cite: (evidence: FindingEvidence, finding: Finding, projectId: string, seen: Map<string, unknown>) => void) => string;
|
|
15
|
+
previous: (account: number | null, fingerprint: string) => FindingOutcome | undefined;
|
|
16
|
+
outcomes: (account: number | null, input: FindingsInput, accepted: string[], agent: FindingsAgent) => FindingOutcome[];
|
|
17
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { findingFingerprint, findingHash, } from "../../meetings/index.js";
|
|
3
|
+
import { factOperationsOver } from "./facts.js";
|
|
4
|
+
import { linkEvidence } from "./memories.js";
|
|
5
|
+
import { requiredThing } from "./notes.js";
|
|
6
|
+
import { taskRowOf } from "./things.js";
|
|
7
|
+
import { workTaskOperationsOver } from "./work-tasks.js";
|
|
8
|
+
const invalid = (message) => {
|
|
9
|
+
throw new CliError("validation_error", message);
|
|
10
|
+
};
|
|
11
|
+
export const findingsAgent = (agent) => {
|
|
12
|
+
if (!agent ||
|
|
13
|
+
typeof agent.bot !== "string" ||
|
|
14
|
+
!agent.bot.trim() ||
|
|
15
|
+
agent.bot.length > 200 ||
|
|
16
|
+
agent.bot === "owner" ||
|
|
17
|
+
(agent.model !== undefined && (typeof agent.model !== "string" || agent.model.length > 200)))
|
|
18
|
+
return invalid("findings must name an agent author independently of their payload");
|
|
19
|
+
return { bot: agent.bot, ...(agent.model === undefined ? {} : { model: agent.model }) };
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* What meeting and evidence findings share: the project and assignee snapshot, earlier decisions on an unchanged
|
|
23
|
+
* candidate, creating the accepted facts and tasks, and the receipt items. Each kind checks its own evidence,
|
|
24
|
+
* including a reference it already saw for another finding.
|
|
25
|
+
*/
|
|
26
|
+
export const findingsCoreOver = (context, kind) => {
|
|
27
|
+
const { database } = context;
|
|
28
|
+
const facts = factOperationsOver(context);
|
|
29
|
+
const tasks = workTaskOperationsOver(context);
|
|
30
|
+
const snapshot = (input, cite) => {
|
|
31
|
+
const seen = new Map();
|
|
32
|
+
for (const finding of input.findings) {
|
|
33
|
+
const project = requiredThing(database, finding.project, "the project");
|
|
34
|
+
if (project.type !== "project")
|
|
35
|
+
return invalid("a finding needs an owner project");
|
|
36
|
+
const projectRow = database
|
|
37
|
+
.prepare("SELECT id,key,name,description,type,scope,updated_at FROM projects WHERE id=? AND account_id IS NULL AND deleted_at IS NULL")
|
|
38
|
+
.get(project.id);
|
|
39
|
+
if (!projectRow)
|
|
40
|
+
throw new CliError("not_found", "the owner project is unavailable");
|
|
41
|
+
seen.set(finding.project, projectRow);
|
|
42
|
+
if (finding.kind === "task" && finding.assignee) {
|
|
43
|
+
const person = requiredThing(database, finding.assignee, "the assignee");
|
|
44
|
+
if (person.type !== "person")
|
|
45
|
+
return invalid("the assignee must be a stored person");
|
|
46
|
+
const personRow = database.prepare("SELECT id,name,updated_at FROM persons WHERE id=?").get(person.id);
|
|
47
|
+
if (!personRow)
|
|
48
|
+
throw new CliError("not_found", "the assignee is unavailable");
|
|
49
|
+
seen.set(finding.assignee, personRow);
|
|
50
|
+
}
|
|
51
|
+
for (const evidence of finding.evidence)
|
|
52
|
+
cite(evidence, finding, String(project.id), seen);
|
|
53
|
+
}
|
|
54
|
+
return findingHash([...seen].sort(([a], [b]) => a.localeCompare(b)));
|
|
55
|
+
};
|
|
56
|
+
const previous = (account, fingerprint) => {
|
|
57
|
+
const row = database
|
|
58
|
+
.prepare("SELECT substr(j.value,1,4096) AS item FROM proposed_actions p,json_each(p.result,'$.items') j WHERE p.account_id IS ? AND p.kind=? AND p.status='approved' AND json_extract(j.value,'$.fingerprint')=? ORDER BY p.id LIMIT 1")
|
|
59
|
+
.get(account, kind, fingerprint);
|
|
60
|
+
return row ? JSON.parse(String(row.item)) : undefined;
|
|
61
|
+
};
|
|
62
|
+
const create = (finding, agent) => {
|
|
63
|
+
const evidence = finding.evidence.map((item) => item.reference);
|
|
64
|
+
if (finding.kind === "fact") {
|
|
65
|
+
const fact = facts.add({
|
|
66
|
+
type: finding.type,
|
|
67
|
+
statement: finding.statement,
|
|
68
|
+
project: finding.project,
|
|
69
|
+
evidence,
|
|
70
|
+
by: agent,
|
|
71
|
+
});
|
|
72
|
+
return facts.accept(fact.ref).ref;
|
|
73
|
+
}
|
|
74
|
+
const task = tasks.create({
|
|
75
|
+
project: finding.project,
|
|
76
|
+
title: finding.title,
|
|
77
|
+
type: finding.type,
|
|
78
|
+
description: `${finding.commitment}: ${finding.reason}`,
|
|
79
|
+
...(finding.dueAt ? { dueAt: new Date(finding.dueAt) } : {}),
|
|
80
|
+
}, agent);
|
|
81
|
+
const id = taskRowOf(database, task.id);
|
|
82
|
+
if (id === undefined)
|
|
83
|
+
throw new CliError("configuration_error", "created finding task is unavailable");
|
|
84
|
+
linkEvidence(context, { type: finding.type, id }, evidence, "agent");
|
|
85
|
+
if (finding.assignee)
|
|
86
|
+
tasks.assign(task.id, { to: finding.assignee, role: "assignee" }, agent);
|
|
87
|
+
return task.ref;
|
|
88
|
+
};
|
|
89
|
+
/** Reuses an earlier decision on an unchanged candidate; refuses to accept one that was rejected. */
|
|
90
|
+
const outcomes = (account, input, accepted, agent) => input.findings.map((finding) => {
|
|
91
|
+
const fingerprint = findingFingerprint(finding);
|
|
92
|
+
const prior = previous(account, fingerprint);
|
|
93
|
+
if (prior) {
|
|
94
|
+
if (accepted.includes(finding.id) && prior.status === "rejected")
|
|
95
|
+
return invalid("this unchanged finding was already rejected; changed evidence needs a new candidate");
|
|
96
|
+
return { ...prior, id: finding.id, reused: true };
|
|
97
|
+
}
|
|
98
|
+
const status = accepted.includes(finding.id) ? "accepted" : "rejected";
|
|
99
|
+
return {
|
|
100
|
+
id: finding.id,
|
|
101
|
+
fingerprint,
|
|
102
|
+
status,
|
|
103
|
+
reference: status === "accepted" ? create(finding, agent) : null,
|
|
104
|
+
reused: false,
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
return { snapshot, previous, outcomes };
|
|
108
|
+
};
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { findingHash, MEETING_FINDINGS_KIND, meetingCueFingerprint, meetingFindingFingerprint, parseMeetingReference, validateMeetingFindings, } from "../../meetings/index.js";
|
|
3
|
-
import {
|
|
4
|
-
import { authorActor
|
|
5
|
-
import { requiredThing } from "./notes.js";
|
|
3
|
+
import { findingsCoreOver } from "./findings-core.js";
|
|
4
|
+
import { authorActor } from "./memories.js";
|
|
6
5
|
import { proposalOperationsOver } from "./proposed-actions.js";
|
|
7
|
-
import { taskRowOf } from "./things.js";
|
|
8
6
|
import { write } from "./transaction.js";
|
|
9
|
-
import { workTaskOperationsOver } from "./work-tasks.js";
|
|
10
7
|
const stale = () => {
|
|
11
8
|
throw new CliError("validation_error", "meeting findings changed since they were read; stage a new batch", {
|
|
12
9
|
reason: "meeting-findings.stale",
|
|
@@ -18,8 +15,6 @@ const invalid = (message) => {
|
|
|
18
15
|
export const meetingFindingsStoreOver = (context) => {
|
|
19
16
|
const { database } = context;
|
|
20
17
|
const proposals = proposalOperationsOver(context);
|
|
21
|
-
const facts = factOperationsOver(context);
|
|
22
|
-
const tasks = workTaskOperationsOver(context);
|
|
23
18
|
const accountId = (key) => {
|
|
24
19
|
if (!key ||
|
|
25
20
|
typeof key.provider !== "string" ||
|
|
@@ -68,109 +63,56 @@ export const meetingFindingsStoreOver = (context) => {
|
|
|
68
63
|
receipt: proposal.result,
|
|
69
64
|
};
|
|
70
65
|
};
|
|
66
|
+
const core = findingsCoreOver(context, MEETING_FINDINGS_KIND);
|
|
71
67
|
const snapshot = (account, input) => {
|
|
72
68
|
let readBytes = 0;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
seen.
|
|
84
|
-
|
|
85
|
-
const person = requiredThing(database, finding.assignee, "the assignee");
|
|
86
|
-
if (person.type !== "person")
|
|
87
|
-
return invalid("the assignee must be a stored person");
|
|
88
|
-
const personRow = database.prepare("SELECT id,name,updated_at FROM persons WHERE id=?").get(person.id);
|
|
89
|
-
if (!personRow)
|
|
90
|
-
throw new CliError("not_found", "the assignee is unavailable");
|
|
91
|
-
seen.set(finding.assignee, personRow);
|
|
92
|
-
}
|
|
93
|
-
for (const evidence of finding.evidence) {
|
|
94
|
-
const parsed = parseMeetingReference(evidence.reference);
|
|
95
|
-
if (parsed.accountId !== account)
|
|
96
|
-
throw new CliError("not_found", "the cited cue belongs to another account");
|
|
97
|
-
const membership = database
|
|
98
|
-
.prepare("SELECT id,updated_at,confirmed,source FROM links WHERE from_type='meeting' AND from_id=? AND to_type='project' AND to_id=? AND kind='member-of' AND anchor IS NULL AND confirmed=1")
|
|
99
|
-
.get(parsed.meetingId, project.id);
|
|
100
|
-
if (!membership)
|
|
101
|
-
return invalid("each cited meeting needs confirmed membership in the finding's project");
|
|
102
|
-
seen.set(`${finding.project}/${parsed.meetingId}`, membership);
|
|
103
|
-
if (seen.has(evidence.reference)) {
|
|
104
|
-
if (seen.get(evidence.reference) !== evidence.fingerprint)
|
|
105
|
-
return stale();
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
const query = "FROM meeting_transcript_rows r JOIN meeting_transcripts t ON t.id=r.meeting_transcript_id JOIN meetings m ON m.id=t.meeting_id WHERE m.id=? AND m.account_id=? AND m.deleted_at IS NULL AND t.id=? AND t.deleted_at IS NULL AND t.superseded_at IS NULL AND r.position=?";
|
|
109
|
-
const params = [parsed.meetingId, account, parsed.transcriptId, parsed.cuePosition];
|
|
110
|
-
const budget = database
|
|
111
|
-
.prepare(`SELECT length(CAST(r.text AS BLOB))+coalesce(length(CAST(r.speaker_name AS BLOB)),0)+coalesce(length(CAST(t.content_hash AS BLOB)),0) AS bytes ${query}`)
|
|
112
|
-
.get(...params);
|
|
113
|
-
if (!budget)
|
|
114
|
-
return stale();
|
|
115
|
-
readBytes += Number(budget.bytes);
|
|
116
|
-
if (!Number.isSafeInteger(readBytes) || readBytes > 65536)
|
|
117
|
-
return invalid("finding evidence exceeds the 65536-byte stored text budget");
|
|
118
|
-
const row = database
|
|
119
|
-
.prepare(`SELECT t.content_hash,r.start_ms,r.end_ms,r.speaker_participant_id,r.speaker_name,r.text ${query}`)
|
|
120
|
-
.get(...params);
|
|
121
|
-
if (!row)
|
|
69
|
+
return core.snapshot(input, (evidence, finding, projectId, seen) => {
|
|
70
|
+
const parsed = parseMeetingReference(evidence.reference);
|
|
71
|
+
if (parsed.accountId !== account)
|
|
72
|
+
throw new CliError("not_found", "the cited cue belongs to another account");
|
|
73
|
+
const membership = database
|
|
74
|
+
.prepare("SELECT id,updated_at,confirmed,source FROM links WHERE from_type='meeting' AND from_id=? AND to_type='project' AND to_id=? AND kind='member-of' AND anchor IS NULL AND confirmed=1")
|
|
75
|
+
.get(parsed.meetingId, projectId);
|
|
76
|
+
if (!membership)
|
|
77
|
+
return invalid("each cited meeting needs confirmed membership in the finding's project");
|
|
78
|
+
seen.set(`${finding.project}/${parsed.meetingId}`, membership);
|
|
79
|
+
if (seen.has(evidence.reference)) {
|
|
80
|
+
if (seen.get(evidence.reference) !== evidence.fingerprint)
|
|
122
81
|
return stale();
|
|
123
|
-
|
|
124
|
-
reference: evidence.reference,
|
|
125
|
-
transcriptId: parsed.transcriptId,
|
|
126
|
-
contentHash: row.content_hash == null ? null : String(row.content_hash),
|
|
127
|
-
position: parsed.cuePosition,
|
|
128
|
-
startMs: Number(row.start_ms),
|
|
129
|
-
endMs: Number(row.end_ms),
|
|
130
|
-
speakerParticipantId: row.speaker_participant_id == null ? null : Number(row.speaker_participant_id),
|
|
131
|
-
speakerName: row.speaker_name == null ? null : String(row.speaker_name),
|
|
132
|
-
text: String(row.text),
|
|
133
|
-
});
|
|
134
|
-
if (fingerprint !== evidence.fingerprint)
|
|
135
|
-
return stale();
|
|
136
|
-
seen.set(evidence.reference, fingerprint);
|
|
82
|
+
return;
|
|
137
83
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
evidence,
|
|
155
|
-
|
|
84
|
+
const query = "FROM meeting_transcript_rows r JOIN meeting_transcripts t ON t.id=r.meeting_transcript_id JOIN meetings m ON m.id=t.meeting_id WHERE m.id=? AND m.account_id=? AND m.deleted_at IS NULL AND t.id=? AND t.deleted_at IS NULL AND t.superseded_at IS NULL AND r.position=?";
|
|
85
|
+
const params = [parsed.meetingId, account, parsed.transcriptId, parsed.cuePosition];
|
|
86
|
+
const budget = database
|
|
87
|
+
.prepare(`SELECT length(CAST(r.text AS BLOB))+coalesce(length(CAST(r.speaker_name AS BLOB)),0)+coalesce(length(CAST(t.content_hash AS BLOB)),0) AS bytes ${query}`)
|
|
88
|
+
.get(...params);
|
|
89
|
+
if (!budget)
|
|
90
|
+
return stale();
|
|
91
|
+
readBytes += Number(budget.bytes);
|
|
92
|
+
if (!Number.isSafeInteger(readBytes) || readBytes > 65536)
|
|
93
|
+
return invalid("finding evidence exceeds the 65536-byte stored text budget");
|
|
94
|
+
const row = database
|
|
95
|
+
.prepare(`SELECT t.content_hash,r.start_ms,r.end_ms,r.speaker_participant_id,r.speaker_name,r.text ${query}`)
|
|
96
|
+
.get(...params);
|
|
97
|
+
if (!row)
|
|
98
|
+
return stale();
|
|
99
|
+
const fingerprint = meetingCueFingerprint({
|
|
100
|
+
reference: evidence.reference,
|
|
101
|
+
transcriptId: parsed.transcriptId,
|
|
102
|
+
contentHash: row.content_hash == null ? null : String(row.content_hash),
|
|
103
|
+
position: parsed.cuePosition,
|
|
104
|
+
startMs: Number(row.start_ms),
|
|
105
|
+
endMs: Number(row.end_ms),
|
|
106
|
+
speakerParticipantId: row.speaker_participant_id == null ? null : Number(row.speaker_participant_id),
|
|
107
|
+
speakerName: row.speaker_name == null ? null : String(row.speaker_name),
|
|
108
|
+
text: String(row.text),
|
|
156
109
|
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
title: finding.title,
|
|
162
|
-
type: finding.type,
|
|
163
|
-
description: `${finding.commitment}: ${finding.reason}`,
|
|
164
|
-
...(finding.dueAt ? { dueAt: new Date(finding.dueAt) } : {}),
|
|
165
|
-
}, agent);
|
|
166
|
-
const id = taskRowOf(database, task.id);
|
|
167
|
-
if (id === undefined)
|
|
168
|
-
throw new CliError("configuration_error", "created finding task is unavailable");
|
|
169
|
-
linkEvidence(context, { type: finding.type, id }, evidence, "agent");
|
|
170
|
-
if (finding.assignee)
|
|
171
|
-
tasks.assign(task.id, { to: finding.assignee, role: "assignee" }, agent);
|
|
172
|
-
return task.ref;
|
|
110
|
+
if (fingerprint !== evidence.fingerprint)
|
|
111
|
+
return stale();
|
|
112
|
+
seen.set(evidence.reference, fingerprint);
|
|
113
|
+
});
|
|
173
114
|
};
|
|
115
|
+
const previous = core.previous;
|
|
174
116
|
return {
|
|
175
117
|
stage: async (key, raw, agent) => {
|
|
176
118
|
const input = validateMeetingFindings(raw);
|
|
@@ -248,23 +190,7 @@ export const meetingFindingsStoreOver = (context) => {
|
|
|
248
190
|
bot: proposal.proposedBy.name,
|
|
249
191
|
...(input.agentModel === null ? {} : { model: input.agentModel }),
|
|
250
192
|
};
|
|
251
|
-
const items = batch.input
|
|
252
|
-
const fingerprint = meetingFindingFingerprint(finding);
|
|
253
|
-
const prior = previous(account, fingerprint);
|
|
254
|
-
if (prior) {
|
|
255
|
-
if (accepted.includes(finding.id) && prior.status === "rejected")
|
|
256
|
-
return invalid("this unchanged finding was already rejected; changed evidence needs a new candidate");
|
|
257
|
-
return { ...prior, id: finding.id, reused: true };
|
|
258
|
-
}
|
|
259
|
-
const status = accepted.includes(finding.id) ? "accepted" : "rejected";
|
|
260
|
-
return {
|
|
261
|
-
id: finding.id,
|
|
262
|
-
fingerprint,
|
|
263
|
-
status,
|
|
264
|
-
reference: status === "accepted" ? create(finding, agent) : null,
|
|
265
|
-
reused: false,
|
|
266
|
-
};
|
|
267
|
-
});
|
|
193
|
+
const items = core.outcomes(account, batch.input, accepted, agent);
|
|
268
194
|
return view(proposals.approve(id, {
|
|
269
195
|
previewFingerprint: batch.fingerprint,
|
|
270
196
|
accepted,
|
|
@@ -63,8 +63,11 @@ export interface ProposedActionsStore {
|
|
|
63
63
|
failed(id: string, error: string): Promise<ProposedAction>;
|
|
64
64
|
judge(id: string, verdict: ProposedAction["verdict"]): Promise<ProposedAction>;
|
|
65
65
|
}
|
|
66
|
-
type ProposalOperations = {
|
|
66
|
+
type ProposalOperations = Omit<{
|
|
67
67
|
[K in keyof ProposedActionsStore]: ProposedActionsStore[K] extends (...args: infer A) => Promise<infer R> ? (...args: A) => R : never;
|
|
68
|
+
}, "approve"> & {
|
|
69
|
+
/** `by` records an agent deciding for the owner; the public store always records the owner. */
|
|
70
|
+
approve(id: string, result?: unknown, by?: Author): ProposedAction;
|
|
68
71
|
};
|
|
69
72
|
export declare const proposalOperationsOver: (context: StoreContext) => ProposalOperations;
|
|
70
73
|
export declare const proposedActionsStoreOver: (context: StoreContext) => ProposedActionsStore;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { MEETING_FINDINGS_KIND } from "../../meetings/index.js";
|
|
3
|
+
import { EVIDENCE_FINDINGS_KIND } from "../evidence-findings.js";
|
|
3
4
|
import { authorActor } from "./memories.js";
|
|
4
5
|
import { requiredThing } from "./notes.js";
|
|
5
6
|
import { referenceOfThing, storedThing } from "./things.js";
|
|
@@ -60,8 +61,8 @@ export const proposalOperationsOver = (context) => {
|
|
|
60
61
|
.run(to, now(), ...values, Number(id));
|
|
61
62
|
return actionOf(rowOf(id));
|
|
62
63
|
});
|
|
63
|
-
const decide = (id, to, result) => {
|
|
64
|
-
const { actor } = authorActor(context,
|
|
64
|
+
const decide = (id, to, result, by = "owner") => {
|
|
65
|
+
const { actor } = authorActor(context, by);
|
|
65
66
|
return move(id, ["proposed"], to, ", decided_by_type = ?, decided_by_id = ?, decided_at = ?, result = ?", [
|
|
66
67
|
actor.type,
|
|
67
68
|
actor.id,
|
|
@@ -109,7 +110,7 @@ export const proposalOperationsOver = (context) => {
|
|
|
109
110
|
.all(status ?? null, status ?? null, Math.min(Math.max(limit, 1), 500))
|
|
110
111
|
.map(actionOf);
|
|
111
112
|
},
|
|
112
|
-
approve: (id, result) => decide(id, "approved", result),
|
|
113
|
+
approve: (id, result, by) => decide(id, "approved", result, by),
|
|
113
114
|
reject: (id) => decide(id, "rejected"),
|
|
114
115
|
executed: (id, result) => move(id, ["approved"], "executed", ", executed_at = ?, result = ?", [
|
|
115
116
|
now(),
|
|
@@ -133,15 +134,22 @@ export const proposalOperationsOver = (context) => {
|
|
|
133
134
|
export const proposedActionsStoreOver = (context) => {
|
|
134
135
|
const operations = proposalOperationsOver(context);
|
|
135
136
|
const generic = (id) => {
|
|
136
|
-
|
|
137
|
+
const kind = operations.get(id).kind;
|
|
138
|
+
if (kind === MEETING_FINDINGS_KIND)
|
|
137
139
|
throw new CliError("validation_error", "review meeting findings through the dedicated findings command", {
|
|
138
140
|
reason: "meeting-findings.dedicated-review",
|
|
139
141
|
});
|
|
142
|
+
if (kind === EVIDENCE_FINDINGS_KIND)
|
|
143
|
+
throw new CliError("validation_error", "review evidence findings through the dedicated findings command", {
|
|
144
|
+
reason: "evidence-findings.dedicated-review",
|
|
145
|
+
});
|
|
140
146
|
};
|
|
141
147
|
return {
|
|
142
148
|
propose: async (input) => {
|
|
143
149
|
if (input.kind === MEETING_FINDINGS_KIND)
|
|
144
150
|
throw new CliError("validation_error", "stage meeting findings through the dedicated findings command");
|
|
151
|
+
if (input.kind === EVIDENCE_FINDINGS_KIND)
|
|
152
|
+
throw new CliError("validation_error", "stage evidence findings through the dedicated findings command");
|
|
145
153
|
return operations.propose(input);
|
|
146
154
|
},
|
|
147
155
|
get: async (id) => operations.get(id),
|
package/dist/store/store.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { ContextEvidenceReader, ContextIndex } from "../context/evidence.js";
|
|
1
|
+
import type { ContextEvidenceReader, ContextIndex, ContextRecordReader } from "../context/evidence.js";
|
|
2
2
|
import { type AccountStore } from "./account-store.js";
|
|
3
3
|
import { type AccountsStore } from "./accounts.js";
|
|
4
4
|
import { type AttachmentsStore } from "./attachments.js";
|
|
5
5
|
import { type ChatsStore } from "./chats.js";
|
|
6
6
|
import { type ConversationsStore } from "./conversations.js";
|
|
7
7
|
import { type CountersStore } from "./counters.js";
|
|
8
|
+
import type { EvidenceFindingsStore } from "./evidence-findings.js";
|
|
8
9
|
import type { MeetingFindingsStore } from "./meeting-findings.js";
|
|
9
10
|
import { type MentionsStore } from "./mentions.js";
|
|
10
11
|
import { type MessagesStore } from "./messages.js";
|
|
@@ -35,6 +36,7 @@ import { type VectorsStore } from "./vectors.js";
|
|
|
35
36
|
export interface MessageStore {
|
|
36
37
|
readonly contextEvidence?: ContextEvidenceReader;
|
|
37
38
|
readonly contextIndex?: ContextIndex;
|
|
39
|
+
readonly contextRecords?: ContextRecordReader;
|
|
38
40
|
/** Accounts, account groups and which account each app profile is. */
|
|
39
41
|
readonly accounts: AccountsStore;
|
|
40
42
|
/** One account's chats: the list, what left, what is complete, their metadata. */
|
|
@@ -77,6 +79,8 @@ export interface MessageStore {
|
|
|
77
79
|
readonly catalog: CatalogStore;
|
|
78
80
|
/** Choices that hold until replaced, each with its evidence. */
|
|
79
81
|
readonly meetingFindings: MeetingFindingsStore;
|
|
82
|
+
/** Cited facts and follow-ups about one project from stored messages, mail and documents, for review. */
|
|
83
|
+
readonly evidenceFindings: EvidenceFindingsStore;
|
|
80
84
|
readonly facts: FactsStore;
|
|
81
85
|
/** What agents concluded: summaries, digests, facts, preferences, each with evidence and a scope. */
|
|
82
86
|
readonly memories: MemoriesStore;
|
package/dist/store/store.js
CHANGED
|
@@ -16,8 +16,9 @@ import { agentActionsStoreOver } from "./sqlite/agent-actions.js";
|
|
|
16
16
|
import * as attachmentTexts from "./sqlite/attachment-texts.js";
|
|
17
17
|
import { botUpdateStoreOver } from "./sqlite/bot-updates.js";
|
|
18
18
|
import { catalogStoreOver } from "./sqlite/catalog.js";
|
|
19
|
-
import { contextEvidenceOver, contextIndexOver } from "./sqlite/context-evidence.js";
|
|
19
|
+
import { contextEvidenceOver, contextIndexOver, contextRecordsOver } from "./sqlite/context-evidence.js";
|
|
20
20
|
import { mailStoreOver } from "./sqlite/emails.js";
|
|
21
|
+
import { evidenceFindingsStoreOver } from "./sqlite/evidence-findings.js";
|
|
21
22
|
import { factsStoreOver } from "./sqlite/facts.js";
|
|
22
23
|
import { involvementStoreOver } from "./sqlite/involvements.js";
|
|
23
24
|
import { knowledgeStoreOver } from "./sqlite/knowledge.js";
|
|
@@ -82,6 +83,7 @@ const storeOver = (context) => {
|
|
|
82
83
|
};
|
|
83
84
|
const contextEvidence = once(() => contextEvidenceOver(context));
|
|
84
85
|
const contextIndex = once(() => contextIndexOver(context, stemmerFor));
|
|
86
|
+
const contextRecords = once(() => contextRecordsOver(context));
|
|
85
87
|
const searches = once(() => searchesStoreOver(context));
|
|
86
88
|
const queries = once(() => queriesStoreOver(context, stemmerFor));
|
|
87
89
|
const searchIndex = once(() => searchIndexStoreOver(context));
|
|
@@ -93,6 +95,7 @@ const storeOver = (context) => {
|
|
|
93
95
|
const notes = once(() => ({ ...notesStoreOver(context), ...noteSearchOver(context) }));
|
|
94
96
|
const catalog = once(() => catalogStoreOver(context));
|
|
95
97
|
const meetingFindings = once(() => meetingFindingsStoreOver(context));
|
|
98
|
+
const evidenceFindings = once(() => evidenceFindingsStoreOver(context));
|
|
96
99
|
const facts = once(() => factsStoreOver(context));
|
|
97
100
|
const memories = once(() => memoriesStoreOver(context));
|
|
98
101
|
const proposedActions = once(() => proposedActionsStoreOver(context));
|
|
@@ -105,6 +108,9 @@ const storeOver = (context) => {
|
|
|
105
108
|
get contextIndex() {
|
|
106
109
|
return contextIndex();
|
|
107
110
|
},
|
|
111
|
+
get contextRecords() {
|
|
112
|
+
return contextRecords();
|
|
113
|
+
},
|
|
108
114
|
meetingVectors: meetingVectorsOver(context),
|
|
109
115
|
get accounts() {
|
|
110
116
|
return areas.accounts().whole;
|
|
@@ -175,6 +181,9 @@ const storeOver = (context) => {
|
|
|
175
181
|
get meetingFindings() {
|
|
176
182
|
return meetingFindings();
|
|
177
183
|
},
|
|
184
|
+
get evidenceFindings() {
|
|
185
|
+
return evidenceFindings();
|
|
186
|
+
},
|
|
178
187
|
get facts() {
|
|
179
188
|
return facts();
|
|
180
189
|
},
|