toolroll 0.9.34 → 0.9.37
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 +102 -0
- package/README.md +7 -0
- package/dist/assignment-adapters.d.ts +12 -48
- package/dist/assignment-adapters.js +33 -43
- package/dist/assignment.d.ts +1 -0
- package/dist/assignment.js +2 -2
- package/dist/browser-workspace.d.ts +3 -3
- package/dist/build-review.d.ts +1 -1
- package/dist/build-review.js +2 -1
- package/dist/builder.js +4 -7
- package/dist/chat-acceptance.d.ts +2 -2
- package/dist/chat-actions.d.ts +18 -12
- package/dist/chat-actions.js +15 -65
- package/dist/chat-ask.js +5 -5
- package/dist/chat-decide.d.ts +14 -1
- package/dist/chat-decide.js +12 -6
- package/dist/chat-delivery-state.d.ts +12 -70
- package/dist/chat-delivery-state.js +17 -135
- package/dist/chat-delivery.js +58 -30
- package/dist/chat-flow.js +8 -6
- package/dist/chat-inbox.js +1 -1
- package/dist/coding-context.d.ts +2 -39
- package/dist/coding-context.js +2 -1
- package/dist/coding-handoff.d.ts +6 -25
- package/dist/coding-handoff.js +11 -5
- package/dist/coding-types.d.ts +2 -26
- package/dist/coding-workspace.d.ts +2 -0
- package/dist/coding-workspace.js +50 -14
- package/dist/contracts/acceptance-terms.d.ts +14 -0
- package/dist/contracts/acceptance-terms.js +16 -0
- package/dist/contracts/chat-actions.d.ts +328 -0
- package/dist/contracts/chat-actions.js +133 -0
- package/dist/contracts/chat-callback-rows.d.ts +59 -0
- package/dist/contracts/chat-callback-rows.js +46 -0
- package/dist/contracts/chat-content.d.ts +276 -0
- package/dist/contracts/chat-content.js +127 -0
- package/dist/contracts/chat-tables.d.ts +6 -0
- package/dist/contracts/chat-tables.js +136 -0
- package/dist/contracts/coding-context.d.ts +97 -0
- package/dist/contracts/coding-context.js +60 -0
- package/dist/contracts/coding-handoff.d.ts +44 -0
- package/dist/contracts/coding-handoff.js +47 -0
- package/dist/contracts/coding-workspace.d.ts +165 -0
- package/dist/contracts/coding-workspace.js +71 -0
- package/dist/contracts/contract.d.ts +40 -7
- package/dist/contracts/contract.js +155 -19
- package/dist/contracts/decision.d.ts +45 -0
- package/dist/contracts/decision.js +45 -0
- package/dist/contracts/discord-callback.d.ts +45 -0
- package/dist/contracts/discord-callback.js +34 -0
- package/dist/contracts/flow-card.d.ts +50 -0
- package/dist/contracts/flow-card.js +46 -0
- package/dist/contracts/flow-propose.d.ts +325 -0
- package/dist/contracts/flow-propose.js +41 -0
- package/dist/contracts/flow-send.d.ts +119 -0
- package/dist/contracts/flow-send.js +112 -0
- package/dist/contracts/flow.d.ts +19405 -0
- package/dist/contracts/flow.js +364 -0
- package/dist/contracts/gateway-tools.d.ts +374 -0
- package/dist/contracts/gateway-tools.js +94 -0
- package/dist/contracts/handoff.d.ts +91 -0
- package/dist/contracts/handoff.js +82 -0
- package/dist/contracts/lead-context.d.ts +165 -0
- package/dist/contracts/lead-context.js +73 -0
- package/dist/contracts/lead-tools.d.ts +1905 -0
- package/dist/contracts/lead-tools.js +265 -0
- package/dist/contracts/memory-pass.d.ts +158 -0
- package/dist/contracts/memory-pass.js +102 -0
- package/dist/contracts/project-knowledge.d.ts +126 -0
- package/dist/contracts/project-knowledge.js +93 -0
- package/dist/contracts/project-memory.d.ts +117 -0
- package/dist/contracts/project-memory.js +51 -0
- package/dist/contracts/project-skills.d.ts +121 -0
- package/dist/contracts/project-skills.js +76 -0
- package/dist/contracts/proof.d.ts +136 -0
- package/dist/contracts/proof.js +110 -0
- package/dist/contracts/review-findings.d.ts +46 -0
- package/dist/contracts/review-findings.js +48 -0
- package/dist/contracts/route.d.ts +484 -0
- package/dist/contracts/route.js +128 -0
- package/dist/contracts/scope.d.ts +105 -0
- package/dist/contracts/scope.js +114 -0
- package/dist/contracts/scout-report.d.ts +234 -0
- package/dist/contracts/scout-report.js +141 -0
- package/dist/contracts/slack-callback.d.ts +43 -0
- package/dist/contracts/slack-callback.js +35 -0
- package/dist/contracts/sort-answer.d.ts +83 -0
- package/dist/contracts/sort-answer.js +77 -0
- package/dist/contracts/stage-output.d.ts +127 -0
- package/dist/contracts/stage-output.js +166 -0
- package/dist/contracts/task-sizing.d.ts +24 -0
- package/dist/contracts/task-sizing.js +28 -0
- package/dist/contracts/teammate-turn.d.ts +43 -0
- package/dist/contracts/teammate-turn.js +51 -0
- package/dist/contracts/teams-callback.d.ts +14 -0
- package/dist/contracts/teams-callback.js +14 -0
- package/dist/contracts/telegram-callback.d.ts +120 -0
- package/dist/contracts/telegram-callback.js +97 -0
- package/dist/contracts/verification-receipt.d.ts +148 -0
- package/dist/contracts/verification-receipt.js +87 -0
- package/dist/decision.d.ts +11 -20
- package/dist/decision.js +86 -167
- package/dist/demo.js +28 -11
- package/dist/discord-chat.js +31 -20
- package/dist/evidence.d.ts +3 -40
- package/dist/evidence.js +5 -1
- package/dist/flow-engine.js +14 -17
- package/dist/flow-gallery.d.ts +3 -2
- package/dist/flow-items.d.ts +4 -13
- package/dist/flow-items.js +1 -15
- package/dist/flow-send.d.ts +6 -37
- package/dist/flow-send.js +18 -14
- package/dist/flow-share.d.ts +10 -32
- package/dist/flow-share.js +72 -84
- package/dist/flow-sort.d.ts +7 -26
- package/dist/flow-sort.js +25 -20
- package/dist/flow-starters.d.ts +2 -1
- package/dist/flow-steps.js +3 -2
- package/dist/flow-triggers.d.ts +15 -72
- package/dist/flow-triggers.js +94 -83
- package/dist/flows.d.ts +37 -220
- package/dist/flows.js +356 -387
- package/dist/invoke.js +4 -4
- package/dist/lead-context.d.ts +4 -2
- package/dist/lead-context.js +24 -17
- package/dist/mate-tools.d.ts +4 -0
- package/dist/mate-tools.js +111 -308
- package/dist/mcp-connect.d.ts +27 -4
- package/dist/mcp-connect.js +92 -17
- package/dist/mcp.js +84 -216
- package/dist/memory-pass.d.ts +17 -42
- package/dist/memory-pass.js +54 -26
- package/dist/operate.js +31 -2
- package/dist/phase-routing.d.ts +19 -72
- package/dist/phase-routing.js +34 -164
- package/dist/plan.d.ts +5 -5
- package/dist/plan.js +5 -25
- package/dist/process-recovery-eligibility.d.ts +2 -2
- package/dist/process-recovery-eligibility.js +13 -8
- package/dist/project-knowledge.d.ts +16 -35
- package/dist/project-knowledge.js +42 -21
- package/dist/project-memory.d.ts +2 -28
- package/dist/project-memory.js +28 -13
- package/dist/project-skills.d.ts +16 -38
- package/dist/project-skills.js +41 -32
- package/dist/project-tools.d.ts +1 -0
- package/dist/project-tools.js +4 -4
- package/dist/proof.d.ts +15 -91
- package/dist/proof.js +90 -249
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +4 -16
- package/dist/result-evidence-readers.d.ts +42 -0
- package/dist/result-evidence-readers.js +91 -0
- package/dist/result-shots.d.ts +1 -1
- package/dist/result-shots.js +2 -1
- package/dist/review-switch.d.ts +3 -8
- package/dist/reviewer.d.ts +8 -12
- package/dist/reviewer.js +11 -35
- package/dist/scope.d.ts +21 -43
- package/dist/scope.js +42 -128
- package/dist/scout-report.d.ts +9 -194
- package/dist/scout-report.js +113 -264
- package/dist/scout.js +15 -11
- package/dist/serve.js +17 -101
- package/dist/slack-chat.js +26 -20
- package/dist/slack-state.js +2 -1
- package/dist/store.d.ts +9 -32
- package/dist/store.js +18 -15
- package/dist/structured-output.js +2 -1
- package/dist/subscription-chat.d.ts +4 -0
- package/dist/subscription-chat.js +4 -1
- package/dist/surface.js +8 -1
- package/dist/task-review-brief.d.ts +69 -0
- package/dist/task-review-brief.js +179 -0
- package/dist/task-sizing.d.ts +3 -27
- package/dist/task-sizing.js +11 -26
- package/dist/task-text.d.ts +0 -5
- package/dist/task-text.js +1 -3
- package/dist/teammate-question.js +7 -5
- package/dist/teammate-work.js +9 -5
- package/dist/teammates.d.ts +13 -64
- package/dist/teammates.js +32 -33
- package/dist/teams-chat.js +20 -11
- package/dist/telegram-flow.js +4 -3
- package/dist/telegram-mate.d.ts +4 -14
- package/dist/telegram-mate.js +8 -7
- package/dist/telegram-team.d.ts +2 -19
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +52 -42
- package/dist/text-limits.d.ts +212 -1
- package/dist/text-limits.js +214 -1
- package/dist/tools-ui.d.ts +2 -1
- package/dist/tools-ui.js +7 -4
- package/dist/verification-evidence.js +24 -12
- package/package.json +1 -1
package/dist/plan.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* the scope proposal is authority-bearing, so it gets the park discipline).
|
|
7
7
|
*/
|
|
8
8
|
import { type AcceptanceCriterion } from "./scope.js";
|
|
9
|
-
import type
|
|
9
|
+
import { type ContractIssue } from "./contracts/contract.js";
|
|
10
10
|
export type PlanProblem = {
|
|
11
11
|
reason: string;
|
|
12
12
|
message: string;
|
|
@@ -71,11 +71,11 @@ export type PlanParseResult = {
|
|
|
71
71
|
problems: PlanProblem[];
|
|
72
72
|
};
|
|
73
73
|
/**
|
|
74
|
-
* A contract issue as a plan problem: the
|
|
75
|
-
*
|
|
76
|
-
*
|
|
74
|
+
* A contract issue as a plan problem (`contractProblemOf`): the reason keeps the codes the rest of the code relies on
|
|
75
|
+
* — `missing-goal`, `bad-touches`, `plan-too-long`, `amendment-too-long`, `acceptance-too-many` — so repair eligibility
|
|
76
|
+
* (document-only problems) and durable outcomes read the same as before the contract.
|
|
77
77
|
*/
|
|
78
|
-
export declare
|
|
78
|
+
export declare const planProblemOf: (issue: ContractIssue) => PlanProblem;
|
|
79
79
|
/**
|
|
80
80
|
* The planner's handoff, read against the plan contract (src/contracts/plan.ts): fail closed, every problem at once,
|
|
81
81
|
* each one naming its path (`acceptance[0].evidence[0]: must be one of ...`). A plan saved before plans carried
|
package/dist/plan.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createHash } from "node:crypto";
|
|
|
9
9
|
import { hasForbiddenControls } from "./decision.js";
|
|
10
10
|
import { parseAcceptanceCriteria } from "./scope.js";
|
|
11
11
|
import { TEXT_LIMITS } from "./text-limits.js";
|
|
12
|
+
import { contractProblemOf } from "./contracts/contract.js";
|
|
12
13
|
import { planPayloadBody, readPlanPayload } from "./contracts/plan.js";
|
|
13
14
|
const EXECUTION_PLAN_SECTIONS = ["approach", "milestones", "dependencies", "risks", "proof"];
|
|
14
15
|
const EXECUTION_PLAN_LIST_CAP = 12;
|
|
@@ -163,32 +164,11 @@ function prose(value, field, cap, required, problems) {
|
|
|
163
164
|
return value;
|
|
164
165
|
}
|
|
165
166
|
/**
|
|
166
|
-
* A contract issue as a plan problem: the
|
|
167
|
-
*
|
|
168
|
-
*
|
|
167
|
+
* A contract issue as a plan problem (`contractProblemOf`): the reason keeps the codes the rest of the code relies on
|
|
168
|
+
* — `missing-goal`, `bad-touches`, `plan-too-long`, `amendment-too-long`, `acceptance-too-many` — so repair eligibility
|
|
169
|
+
* (document-only problems) and durable outcomes read the same as before the contract.
|
|
169
170
|
*/
|
|
170
|
-
export
|
|
171
|
-
const at = issue.path;
|
|
172
|
-
const reason = (() => {
|
|
173
|
-
switch (issue.kind) {
|
|
174
|
-
case "required":
|
|
175
|
-
case "empty":
|
|
176
|
-
case "too-few":
|
|
177
|
-
return `missing-${at}`;
|
|
178
|
-
case "too-long":
|
|
179
|
-
return `${at}-too-long`;
|
|
180
|
-
case "too-many":
|
|
181
|
-
return `${at}-too-many`;
|
|
182
|
-
case "unknown-key":
|
|
183
|
-
return `${at}-unknown-key`;
|
|
184
|
-
case "newer-version":
|
|
185
|
-
return "newer-version";
|
|
186
|
-
default:
|
|
187
|
-
return `bad-${at}`;
|
|
188
|
-
}
|
|
189
|
-
})();
|
|
190
|
-
return { reason, message: issue.line };
|
|
191
|
-
}
|
|
171
|
+
export const planProblemOf = contractProblemOf;
|
|
192
172
|
/** scope.ts's rubric problem as a path-named line: `acceptance[0].id: is over 40 bytes`. */
|
|
193
173
|
function acceptanceLine(problem) {
|
|
194
174
|
const named = /^(acceptance(?:\[\d+\])?(?:\.[A-Za-z]+)?) (.*)$/.exec(problem.message);
|
|
@@ -37,7 +37,7 @@ export declare function preparedCandidateObserverGapEligibility(store: Store, in
|
|
|
37
37
|
leaseId: string;
|
|
38
38
|
role: "builder";
|
|
39
39
|
outcome: "built" | "no-change";
|
|
40
|
-
committed: boolean
|
|
40
|
+
committed: boolean;
|
|
41
41
|
startedAt: string;
|
|
42
42
|
finishedAt: string;
|
|
43
43
|
branch: string;
|
|
@@ -81,7 +81,7 @@ export declare function preparedCandidateObserverGapEligibility(store: Store, in
|
|
|
81
81
|
readonly state: import("./proof.js").CriterionMatrixState;
|
|
82
82
|
readonly detail: readonly string[];
|
|
83
83
|
readonly answered: readonly {
|
|
84
|
-
readonly kind:
|
|
84
|
+
readonly kind: "changed-path" | "check" | "manual-review" | "screenshot";
|
|
85
85
|
readonly ref: string;
|
|
86
86
|
}[];
|
|
87
87
|
readonly review: {
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
9
|
import { normalizeBootId } from "./boot-identity.js";
|
|
10
10
|
import { scopeApprovedForDispatch } from "./dispatch.js";
|
|
11
|
+
import { parseHandoffArtifact } from "./contracts/handoff.js";
|
|
12
|
+
import { readVerificationReceipt } from "./contracts/verification-receipt.js";
|
|
11
13
|
import { readVerifiedArtifact } from "./evidence.js";
|
|
12
14
|
import { ownedProcessCount, runOwnerTag } from "./exec.js";
|
|
13
15
|
import { scopeAuthorityOf } from "./scope.js";
|
|
@@ -97,11 +99,11 @@ export function preparedCandidateObserverGapEligibility(store, input) {
|
|
|
97
99
|
const gate = verificationEvidence(store, evidenceRoot, runId);
|
|
98
100
|
if (!gate.ok || gate.bytes === null)
|
|
99
101
|
return refuse("verification-evidence-unavailable");
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
// A legacy log-header view is not a sealed receipt: the strict schema refuses its extra keys.
|
|
103
|
+
const view = JSON.parse(gate.bytes);
|
|
104
|
+
const receipt = readVerificationReceipt(view);
|
|
105
|
+
if (!receipt.ok || view.version !== 1 || receipt.value.reusedFrom !== undefined || receipt.value.executedHere !== undefined ||
|
|
106
|
+
receipt.value.result.configured !== true || receipt.value.result.ran !== true || receipt.value.result.exitCode !== 0 ||
|
|
105
107
|
!instant(gateArtifact.createdAt) || Date.parse(gateArtifact.createdAt) < Date.parse(run.startedAt) ||
|
|
106
108
|
Date.parse(gateArtifact.createdAt) > Date.parse(machine.decidedAt) ||
|
|
107
109
|
Date.parse(gateArtifact.createdAt) > Date.parse(run.finishedAt))
|
|
@@ -115,12 +117,15 @@ export function preparedCandidateObserverGapEligibility(store, input) {
|
|
|
115
117
|
const saved = readVerifiedArtifact(evidenceRoot, artifact);
|
|
116
118
|
if (!saved.ok)
|
|
117
119
|
return refuse("machine-prepared-handoff-unavailable");
|
|
118
|
-
const
|
|
120
|
+
const read = parseHandoffArtifact(saved.content.toString("utf8"));
|
|
121
|
+
if (!read.ok)
|
|
122
|
+
return refuse("machine-prepared-handoff-mismatch");
|
|
123
|
+
const handoff = read.value;
|
|
119
124
|
const conclusion = `Prepared candidate ${scope.candidate} was checked out by the machine; no agent ran. ${run.outcome === "no-change" ? "The branch already matched it." : "The sealed diff spans this task's base to that candidate."}`;
|
|
120
|
-
if (handoff.
|
|
125
|
+
if (handoff.runId !== runId || handoff.taskId !== ref.externalId || handoff.sessionId !== null ||
|
|
121
126
|
handoff.provider !== run.provider || handoff.branch !== run.branch || handoff.worktree !== run.worktree ||
|
|
122
127
|
handoff.base !== run.baseRevision || handoff.head !== run.headRevision || handoff.outcome !== run.outcome ||
|
|
123
|
-
handoff.committed !== run.committed || handoff.freshness
|
|
128
|
+
handoff.committed !== run.committed || handoff.freshness.currentAsOf !== run.headRevision ||
|
|
124
129
|
handoff.conclusion !== conclusion || run.handoff !== conclusion ||
|
|
125
130
|
!instant(artifact.createdAt) || Date.parse(artifact.createdAt) < Date.parse(run.startedAt) ||
|
|
126
131
|
Date.parse(artifact.createdAt) > Date.parse(gateArtifact.createdAt))
|
|
@@ -1,26 +1,7 @@
|
|
|
1
|
-
import { type DecisionLine } from './project-memory.js';
|
|
2
|
-
import { type RepositoryContext } from './repository-context.js';
|
|
3
1
|
import type { Store } from './store.js';
|
|
2
|
+
import { type Knowledge, type KnowledgeDraft, type KnowledgeReference, type KnowledgeSelection } from './contracts/project-knowledge.js';
|
|
4
3
|
export declare const KNOWLEDGE_SCHEMA = "\nCREATE TABLE IF NOT EXISTS project_knowledge (\n repo TEXT PRIMARY KEY, identity TEXT NOT NULL, revision INTEGER NOT NULL, payload TEXT NOT NULL, sha TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS knowledge_change (\n id INTEGER PRIMARY KEY, repo TEXT NOT NULL, identity TEXT NOT NULL, revision INTEGER NOT NULL,\n actor TEXT NOT NULL, at TEXT NOT NULL, payload TEXT NOT NULL, sha TEXT NOT NULL,\n UNIQUE(repo,identity,revision)\n);\nCREATE TABLE IF NOT EXISTS knowledge_snapshot (\n run INTEGER PRIMARY KEY REFERENCES run(id), repo TEXT NOT NULL, identity TEXT NOT NULL,\n payload TEXT NOT NULL, sha TEXT NOT NULL\n);\n-- A saved project identity carried forward (a restart renumbered the volume, or an approver applied it).\nCREATE TABLE IF NOT EXISTS project_identity_carry (\n id INTEGER PRIMARY KEY, repo TEXT NOT NULL, from_identity TEXT NOT NULL, to_identity TEXT NOT NULL,\n actor TEXT NOT NULL, at TEXT NOT NULL, how TEXT NOT NULL CHECK(how IN ('automatic','approver')),\n UNIQUE(repo,from_identity,to_identity)\n);\nCREATE TRIGGER IF NOT EXISTS project_identity_carry_no_update BEFORE UPDATE ON project_identity_carry BEGIN SELECT RAISE(ABORT,'Project identity history is immutable'); END;\nCREATE TRIGGER IF NOT EXISTS project_identity_carry_no_delete BEFORE DELETE ON project_identity_carry BEGIN SELECT RAISE(ABORT,'Project identity history is immutable'); END;\nCREATE TRIGGER IF NOT EXISTS knowledge_change_no_update BEFORE UPDATE ON knowledge_change BEGIN SELECT RAISE(ABORT,'Knowledge history is immutable'); END;\nCREATE TRIGGER IF NOT EXISTS knowledge_change_no_delete BEFORE DELETE ON knowledge_change BEGIN SELECT RAISE(ABORT,'Knowledge history is immutable'); END;\nCREATE TRIGGER IF NOT EXISTS knowledge_snapshot_no_update BEFORE UPDATE ON knowledge_snapshot BEGIN SELECT RAISE(ABORT,'Knowledge context is immutable'); END;\nCREATE TRIGGER IF NOT EXISTS knowledge_snapshot_no_delete BEFORE DELETE ON knowledge_snapshot BEGIN SELECT RAISE(ABORT,'Knowledge context is immutable'); END;\n";
|
|
5
|
-
export type KnowledgeReference
|
|
6
|
-
id: string;
|
|
7
|
-
title: string;
|
|
8
|
-
content: string;
|
|
9
|
-
path: string | null;
|
|
10
|
-
sourceSha: string | null;
|
|
11
|
-
sourceRevision: string | null;
|
|
12
|
-
};
|
|
13
|
-
export type Knowledge = {
|
|
14
|
-
instructions: string;
|
|
15
|
-
references: KnowledgeReference[];
|
|
16
|
-
};
|
|
17
|
-
export type KnowledgeDraft = {
|
|
18
|
-
instructions?: string;
|
|
19
|
-
title?: string;
|
|
20
|
-
content?: string;
|
|
21
|
-
path?: string;
|
|
22
|
-
id?: string;
|
|
23
|
-
};
|
|
4
|
+
export type { Knowledge, KnowledgeDraft, KnowledgeReference, KnowledgeSelection };
|
|
24
5
|
export declare const STALE_KNOWLEDGE = "This project's saved knowledge is from before a change; review and apply it";
|
|
25
6
|
export declare function knowledgeView(store: Store, repo: string, actor: string): {
|
|
26
7
|
repo: string;
|
|
@@ -53,7 +34,20 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
53
34
|
decisions?: never;
|
|
54
35
|
} | {
|
|
55
36
|
stale?: never;
|
|
56
|
-
decision:
|
|
37
|
+
decision: {
|
|
38
|
+
repo: string;
|
|
39
|
+
revision: number;
|
|
40
|
+
claim: string;
|
|
41
|
+
why: string;
|
|
42
|
+
status: "active" | "retired" | "superseded";
|
|
43
|
+
supersedes: number | null;
|
|
44
|
+
decidedBy: string;
|
|
45
|
+
decidedAt: string;
|
|
46
|
+
sourceKind: "backward-pass" | "conversation" | "manual" | "result" | "task";
|
|
47
|
+
sourceRef: string | null;
|
|
48
|
+
recordedBy: string;
|
|
49
|
+
id: number;
|
|
50
|
+
};
|
|
57
51
|
notice: string;
|
|
58
52
|
title?: never;
|
|
59
53
|
content?: never;
|
|
@@ -110,19 +104,6 @@ export declare function changeKnowledge(store: Store, args: {
|
|
|
110
104
|
* Returns the new revision, or null when nothing was waiting.
|
|
111
105
|
*/
|
|
112
106
|
export declare function applySavedKnowledge(store: Store, repo: string, actor: string, now?: Date): number | null;
|
|
113
|
-
export type KnowledgeSelection = {
|
|
114
|
-
version: 1;
|
|
115
|
-
revision: number;
|
|
116
|
-
instructions: string;
|
|
117
|
-
references: KnowledgeReference[];
|
|
118
|
-
omitted: {
|
|
119
|
-
title: string;
|
|
120
|
-
reason: string;
|
|
121
|
-
}[];
|
|
122
|
-
inheritedFrom: number | null;
|
|
123
|
-
repository?: RepositoryContext;
|
|
124
|
-
decisions?: DecisionLine[];
|
|
125
|
-
};
|
|
126
107
|
/** Read the same bounded, source-checked selection without inventing a worker run. */
|
|
127
108
|
export declare function selectProjectKnowledge(store: Store, args: {
|
|
128
109
|
repo: string;
|
|
@@ -5,6 +5,9 @@ import { currentActor } from './actor.js';
|
|
|
5
5
|
import { decisionLines, getDecision, listDecisions } from './project-memory.js';
|
|
6
6
|
import { scanForSecrets } from './evidence.js';
|
|
7
7
|
import { repositoryContextRead } from './repository-context.js';
|
|
8
|
+
import { TEXT_LIMITS } from './text-limits.js';
|
|
9
|
+
import { contractError } from './contracts/contract.js';
|
|
10
|
+
import { KNOWLEDGE_COUNTS, KNOWLEDGE_SELECTION_VERSION, KNOWLEDGE_VERSION, knowledgeSchema, knowledgeSelectionSchema, readKnowledge, readKnowledgeSelection } from './contracts/project-knowledge.js';
|
|
8
11
|
export const KNOWLEDGE_SCHEMA = `
|
|
9
12
|
CREATE TABLE IF NOT EXISTS project_knowledge (
|
|
10
13
|
repo TEXT PRIMARY KEY, identity TEXT NOT NULL, revision INTEGER NOT NULL, payload TEXT NOT NULL, sha TEXT NOT NULL
|
|
@@ -49,10 +52,21 @@ export const STALE_KNOWLEDGE = "This project's saved knowledge is from before a
|
|
|
49
52
|
const STALE_OMITTED = { title: 'Saved project knowledge', reason: 'Saved knowledge is from before a change; it was not applied' };
|
|
50
53
|
/** Revisions under any identity this project accepts, as a bound JSON list. */
|
|
51
54
|
const IN_ACCEPTED = 'identity IN (SELECT value FROM json_each(?))';
|
|
55
|
+
/** The stored bytes are checked against their digest first; only then are they parsed, and upgraded in memory. */
|
|
52
56
|
function decode(payload, sha) {
|
|
53
57
|
if (learningSha(payload) !== sha)
|
|
54
58
|
throw Error('Project knowledge could not be verified.');
|
|
55
|
-
|
|
59
|
+
const read = readKnowledge(JSON.parse(payload));
|
|
60
|
+
if (!read.ok)
|
|
61
|
+
throw Error('Project knowledge could not be verified.');
|
|
62
|
+
return read.value;
|
|
63
|
+
}
|
|
64
|
+
/** Knowledge (or a selection) as it is written: through its schema, so a refusal names the field. */
|
|
65
|
+
function encode(schema, value, what) {
|
|
66
|
+
const read = schema.safeParse(value, { reportInput: true });
|
|
67
|
+
if (!read.success)
|
|
68
|
+
throw Error(`${what} could not be saved: ${contractError(read.error).join('; ')}`);
|
|
69
|
+
return JSON.stringify(read.data);
|
|
56
70
|
}
|
|
57
71
|
/** Saved knowledge under another identity is stale: it is returned for review, never applied. */
|
|
58
72
|
function current(store, repo, accepted) {
|
|
@@ -73,9 +87,9 @@ function document(repo, revision, path) {
|
|
|
73
87
|
if (!match)
|
|
74
88
|
throw Error('This reference must be a committed text file, not a folder or link.');
|
|
75
89
|
const sha = match[1];
|
|
76
|
-
if (Number(git(repo, ['cat-file', '-s', sha])) >
|
|
90
|
+
if (Number(git(repo, ['cat-file', '-s', sha])) > TEXT_LIMITS.knowledgeReferenceBytes)
|
|
77
91
|
throw Error('Choose a shorter reference (up to 12 KB).');
|
|
78
|
-
return { content: clean(git(repo, ['cat-file', 'blob', sha]),
|
|
92
|
+
return { content: clean(git(repo, ['cat-file', 'blob', sha]), TEXT_LIMITS.knowledgeReferenceBytes), sha };
|
|
79
93
|
}
|
|
80
94
|
export function knowledgeView(store, repo, actor) {
|
|
81
95
|
const { identity, accepted } = admission(store, repo, actor), { stale, ...value } = current(store, repo, accepted);
|
|
@@ -128,7 +142,7 @@ export function changeKnowledge(store, args, now = new Date()) {
|
|
|
128
142
|
throw Error('Knowledge changed in another window. Review your draft below before saving again.');
|
|
129
143
|
let knowledge = existing.knowledge;
|
|
130
144
|
if (args.action === 'instructions')
|
|
131
|
-
knowledge.instructions = clean(args.draft.instructions ?? '',
|
|
145
|
+
knowledge.instructions = clean(args.draft.instructions ?? '', TEXT_LIMITS.knowledgeInstructionsBytes);
|
|
132
146
|
else if (args.action === 'restore') {
|
|
133
147
|
const row = store.handle.prepare(`SELECT payload,sha FROM knowledge_change WHERE repo=? AND ${IN_ACCEPTED} AND revision=?`).get(args.repo, JSON.stringify([...accepted]), args.restore ?? -1);
|
|
134
148
|
if (!row)
|
|
@@ -141,26 +155,26 @@ export function changeKnowledge(store, args, now = new Date()) {
|
|
|
141
155
|
knowledge.references = knowledge.references.filter(r => r.id !== args.draft.id);
|
|
142
156
|
}
|
|
143
157
|
else if (args.action === 'save') {
|
|
144
|
-
const title = clean(args.draft.title ?? '',
|
|
158
|
+
const title = clean(args.draft.title ?? '', TEXT_LIMITS.knowledgeTitleBytes);
|
|
145
159
|
if (!title)
|
|
146
160
|
throw Error('Give this reference a short name.');
|
|
147
161
|
const id = args.draft.id || learningSha(`${identity}:${existing.revision + 1}:${title}`).slice(0, 20);
|
|
148
162
|
if (args.draft.id && !knowledge.references.some(r => r.id === id))
|
|
149
163
|
throw Error('That reference is no longer available.');
|
|
150
|
-
const path = clean(args.draft.path ?? '',
|
|
164
|
+
const path = clean(args.draft.path ?? '', TEXT_LIMITS.knowledgePathBytes) || null;
|
|
151
165
|
const sourceRevision = path ? git(args.repo, ['rev-parse', 'HEAD']) : null;
|
|
152
166
|
const source = path ? document(args.repo, sourceRevision, path) : null;
|
|
153
|
-
const content = source?.content ?? clean(args.draft.content ?? '',
|
|
167
|
+
const content = source?.content ?? clean(args.draft.content ?? '', TEXT_LIMITS.knowledgeReferenceBytes);
|
|
154
168
|
if (!content)
|
|
155
169
|
throw Error('Add reference text or select a committed project document.');
|
|
156
170
|
const ref = { id, title, content, path, sourceSha: source?.sha ?? null, sourceRevision };
|
|
157
171
|
knowledge.references = [...knowledge.references.filter(r => r.id !== id), ref];
|
|
158
|
-
if (knowledge.references.length >
|
|
172
|
+
if (knowledge.references.length > KNOWLEDGE_COUNTS.references)
|
|
159
173
|
throw Error('Keep up to 12 focused references. Remove one before adding another.');
|
|
160
174
|
}
|
|
161
175
|
else
|
|
162
176
|
throw Error('Choose a supported knowledge action.');
|
|
163
|
-
const payload =
|
|
177
|
+
const payload = encode(knowledgeSchema, { version: KNOWLEDGE_VERSION, ...knowledge }, 'Project knowledge'), sha = learningSha(payload), revision = existing.revision + 1;
|
|
164
178
|
store.handle.prepare('INSERT INTO knowledge_change(repo,identity,revision,actor,at,payload,sha) VALUES (?,?,?,?,?,?,?)').run(args.repo, identity, revision, args.actor, now.toISOString(), payload, sha);
|
|
165
179
|
store.handle.prepare('INSERT INTO project_knowledge VALUES (?,?,?,?,?) ON CONFLICT(repo) DO UPDATE SET identity=excluded.identity,revision=excluded.revision,payload=excluded.payload,sha=excluded.sha').run(args.repo, identity, revision, payload, sha);
|
|
166
180
|
});
|
|
@@ -191,7 +205,7 @@ function select(store, repo, accepted, query, head) {
|
|
|
191
205
|
const ranked = knowledge.references.map(ref => ({ ref, score: [...tokens(`${ref.title} ${ref.path ?? ''}`)].reduce((n, t) => n + (q.has(t) ? 3 : 0), 0) + [...tokens(ref.content)].reduce((n, t) => n + (q.has(t) ? 1 : 0), 0) })).sort((a, b) => b.score - a.score || a.ref.id.localeCompare(b.ref.id));
|
|
192
206
|
const references = [];
|
|
193
207
|
for (const { ref, score } of ranked) {
|
|
194
|
-
let reason = score === 0 ? 'Not relevant to this task' : references.length >=
|
|
208
|
+
let reason = score === 0 ? 'Not relevant to this task' : references.length >= KNOWLEDGE_COUNTS.selected ? 'More relevant references selected' : '';
|
|
195
209
|
if (ref.path) {
|
|
196
210
|
try {
|
|
197
211
|
if (document(repo, head, ref.path).sha !== ref.sourceSha)
|
|
@@ -201,7 +215,7 @@ function select(store, repo, accepted, query, head) {
|
|
|
201
215
|
reason = 'Source unavailable; refresh this reference';
|
|
202
216
|
}
|
|
203
217
|
}
|
|
204
|
-
if (!reason && Buffer.byteLength(JSON.stringify({ instructions: knowledge.instructions, references: [...references, ref] })) >
|
|
218
|
+
if (!reason && Buffer.byteLength(JSON.stringify({ instructions: knowledge.instructions, references: [...references, ref] })) > TEXT_LIMITS.knowledgeContextBytes)
|
|
205
219
|
reason = 'Context size limit';
|
|
206
220
|
if (reason)
|
|
207
221
|
omitted.push({ title: ref.title, reason });
|
|
@@ -210,8 +224,8 @@ function select(store, repo, accepted, query, head) {
|
|
|
210
224
|
}
|
|
211
225
|
// Settled decisions ride the same frozen record, one line each; the why
|
|
212
226
|
// loads on demand by id, so nothing is repeated between brief and store.
|
|
213
|
-
const decisions = decisionLines(store, repo, query, { limit:
|
|
214
|
-
return { version:
|
|
227
|
+
const decisions = decisionLines(store, repo, query, { limit: KNOWLEDGE_COUNTS.decisions, bytes: Math.max(0, Math.min(TEXT_LIMITS.knowledgeDecisionsBytes, TEXT_LIMITS.knowledgeContextBytes - Buffer.byteLength(JSON.stringify({ instructions: knowledge.instructions, references })) - 200)) });
|
|
228
|
+
return { version: KNOWLEDGE_SELECTION_VERSION, revision, instructions: knowledge.instructions, references, omitted, inheritedFrom: null, ...(decisions.length ? { decisions } : {}) };
|
|
215
229
|
}
|
|
216
230
|
/** Read the same bounded, source-checked selection without inventing a worker run. */
|
|
217
231
|
export function selectProjectKnowledge(store, args) {
|
|
@@ -227,10 +241,17 @@ export function readKnowledgeSnapshot(store, runId) {
|
|
|
227
241
|
if (!row)
|
|
228
242
|
return null;
|
|
229
243
|
const run = store.getRun(runId), repo = run && store.refForId(run.taskRef)?.repo;
|
|
230
|
-
const
|
|
244
|
+
const unverified = () => Error('The context saved for this run could not be verified.');
|
|
245
|
+
// The stored bytes are checked against their digest before they are parsed.
|
|
246
|
+
if (!repo || row['repo'] !== repo || learningSha(String(row['payload'])) !== row['sha'])
|
|
247
|
+
throw unverified();
|
|
248
|
+
const read = readKnowledgeSelection(JSON.parse(String(row['payload'])));
|
|
249
|
+
if (!read.ok)
|
|
250
|
+
throw unverified();
|
|
251
|
+
const selection = read.value;
|
|
231
252
|
const empty = row['identity'] === 'unconfigured' && unconfigured(selection);
|
|
232
|
-
if (!
|
|
233
|
-
throw
|
|
253
|
+
if (!empty && !acceptedIdentities(store, repo).has(String(row['identity'])))
|
|
254
|
+
throw unverified();
|
|
234
255
|
return selection;
|
|
235
256
|
}
|
|
236
257
|
/** Freeze at provider admission. Reviewers see the builder's exact project context. */
|
|
@@ -259,18 +280,18 @@ export function knowledgeContext(store, runId, cacheRoot, validatedBuilderBase)
|
|
|
259
280
|
const configured = !!store.handle.prepare('SELECT 1 FROM project_knowledge WHERE repo=?').get(repo);
|
|
260
281
|
// A legacy source without a snapshot did not receive this feature's
|
|
261
282
|
// context. Never give its reviewer newly configured project guidance.
|
|
262
|
-
selection = inherited ? { ...inherited, inheritedFrom: parent.id } : !configured || parent ? { version:
|
|
283
|
+
selection = inherited ? { ...inherited, inheritedFrom: parent.id } : !configured || parent ? { version: KNOWLEDGE_SELECTION_VERSION, revision: 0, instructions: '', references: [], omitted: [], inheritedFrom: parent?.id ?? null } : select(store, repo, acceptedIdentities(store, repo), `${store.getTask(ref.externalId)?.title ?? ''} ${scope?.goal ?? ''} ${(scope?.touches ?? []).join(' ')}`, contextBase || git(repo, ['rev-parse', 'HEAD']));
|
|
263
284
|
// Optional source selection is captured once from this crew's actual
|
|
264
285
|
// checkout. It is reused with the immutable run snapshot on resume.
|
|
265
286
|
// Index/source failure stays context metadata, never an admission gate.
|
|
266
287
|
if (!parent && run.worktree && contextBase) {
|
|
267
|
-
const available =
|
|
268
|
-
if (available >=
|
|
269
|
-
selection.repository = repositoryContextRead({ repo: run.worktree, project: repo, baseRevision: contextBase, audience: 'crew', maxBytes: Math.min(
|
|
288
|
+
const available = TEXT_LIMITS.knowledgeContextBytes - Buffer.byteLength(JSON.stringify(selection)) - 30;
|
|
289
|
+
if (available >= TEXT_LIMITS.knowledgeRepositoryMinBytes)
|
|
290
|
+
selection.repository = repositoryContextRead({ repo: run.worktree, project: repo, baseRevision: contextBase, audience: 'crew', maxBytes: Math.min(TEXT_LIMITS.knowledgeRepositoryBytes, available), ...(cacheRoot === undefined ? {} : { cacheRoot }), query: `${store.getTask(ref.externalId)?.title ?? ''} ${scope?.goal ?? ''} ${(scope?.touches ?? []).join(' ')}` });
|
|
270
291
|
else
|
|
271
292
|
selection.omitted.push({ title: 'Repository context', reason: 'Context size limit' });
|
|
272
293
|
}
|
|
273
|
-
const payload =
|
|
294
|
+
const payload = encode(knowledgeSelectionSchema, selection, 'Project context');
|
|
274
295
|
store.handle.prepare('INSERT INTO knowledge_snapshot VALUES (?,?,?,?,?)').run(runId, repo, unconfigured(selection) ? 'unconfigured' : learningIdentity(repo), payload, learningSha(payload));
|
|
275
296
|
}
|
|
276
297
|
// JSON escapes ordinary newlines, but not Unicode line separators. Keep
|
package/dist/project-memory.d.ts
CHANGED
|
@@ -1,28 +1,7 @@
|
|
|
1
1
|
import type { Store } from './store.js';
|
|
2
|
+
import { type Decision, type DecisionDraft, type DecisionLine } from './contracts/project-memory.js';
|
|
3
|
+
export type { Decision, DecisionDraft, DecisionLine };
|
|
2
4
|
export declare const MEMORY_SCHEMA = "\nCREATE TABLE IF NOT EXISTS project_decision (\n id INTEGER PRIMARY KEY, repo TEXT NOT NULL, identity TEXT NOT NULL, revision INTEGER NOT NULL DEFAULT 1,\n claim TEXT NOT NULL, why TEXT NOT NULL,\n status TEXT NOT NULL CHECK(status IN ('active','superseded','retired')),\n supersedes INTEGER REFERENCES project_decision(id),\n decided_by TEXT NOT NULL, decided_at TEXT NOT NULL,\n source_kind TEXT NOT NULL CHECK(source_kind IN ('conversation','task','result','manual','backward-pass')),\n source_ref TEXT, recorded_by TEXT NOT NULL, sha TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS project_decision_repo ON project_decision(repo, status, id);\nCREATE TABLE IF NOT EXISTS decision_change (\n id INTEGER PRIMARY KEY, decision INTEGER NOT NULL REFERENCES project_decision(id), revision INTEGER NOT NULL,\n actor TEXT NOT NULL, at TEXT NOT NULL, action TEXT NOT NULL, payload TEXT NOT NULL, sha TEXT NOT NULL,\n UNIQUE(decision, revision)\n);\nCREATE TRIGGER IF NOT EXISTS decision_change_no_update BEFORE UPDATE ON decision_change BEGIN SELECT RAISE(ABORT,'Decision history is immutable'); END;\nCREATE TRIGGER IF NOT EXISTS decision_change_no_delete BEFORE DELETE ON decision_change BEGIN SELECT RAISE(ABORT,'Decision history is immutable'); END;\nCREATE TRIGGER IF NOT EXISTS project_decision_no_delete BEFORE DELETE ON project_decision BEGIN SELECT RAISE(ABORT,'Decisions are retired, never deleted'); END;\nCREATE VIRTUAL TABLE IF NOT EXISTS memory_search USING fts5(kind UNINDEXED, ref UNINDEXED, repo UNINDEXED, scope UNINDEXED, title, body, tokenize='unicode61');\n-- The backward pass (memory-pass.ts): sessions analysed once per surface\n-- version, gaps that accumulate corroboration across runs, and the\n-- proposals a person accepts or rejects. Rejections are remembered.\nCREATE TABLE IF NOT EXISTS memory_session (\n id TEXT PRIMARY KEY, repo TEXT NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('claude','codex','lead','crew')),\n source TEXT NOT NULL, seen_at TEXT NOT NULL, surface TEXT NOT NULL, trace_sha TEXT NOT NULL,\n analyzed_at TEXT, verdict TEXT, problem TEXT\n);\nCREATE INDEX IF NOT EXISTS memory_session_repo ON memory_session(repo, seen_at);\nCREATE TABLE IF NOT EXISTS memory_gap (\n id INTEGER PRIMARY KEY, repo TEXT NOT NULL, key TEXT NOT NULL, mistake TEXT NOT NULL, proposed TEXT NOT NULL,\n domain TEXT NOT NULL CHECK(domain IN ('project','orchestration')), first_seen TEXT NOT NULL, last_seen TEXT NOT NULL,\n retired_at TEXT, retired_reason TEXT, UNIQUE(repo, key)\n);\nCREATE TABLE IF NOT EXISTS memory_sighting (\n gap INTEGER NOT NULL REFERENCES memory_gap(id), session TEXT NOT NULL REFERENCES memory_session(id),\n at TEXT NOT NULL, quote TEXT NOT NULL, PRIMARY KEY(gap, session)\n);\nCREATE TABLE IF NOT EXISTS memory_proposal (\n id INTEGER PRIMARY KEY, repo TEXT NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('instruction-add','instruction-remove','decision-add')),\n fingerprint TEXT NOT NULL, title TEXT NOT NULL, rationale TEXT NOT NULL, before_text TEXT, after_text TEXT NOT NULL,\n evidence TEXT NOT NULL, sessions INTEGER NOT NULL, status TEXT NOT NULL CHECK(status IN ('pending','accepted','rejected','stale')),\n created_at TEXT NOT NULL, decided_by TEXT, decided_at TEXT, surface TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS memory_proposal_pending ON memory_proposal(repo, status, fingerprint);\nCREATE TABLE IF NOT EXISTS memory_rejection (\n repo TEXT NOT NULL, fingerprint TEXT NOT NULL, rejected_at TEXT NOT NULL, rejected_by TEXT NOT NULL, sessions INTEGER NOT NULL, PRIMARY KEY(repo, fingerprint)\n);\n";
|
|
3
|
-
export type Decision = {
|
|
4
|
-
id: number;
|
|
5
|
-
repo: string;
|
|
6
|
-
revision: number;
|
|
7
|
-
claim: string;
|
|
8
|
-
why: string;
|
|
9
|
-
status: 'active' | 'superseded' | 'retired';
|
|
10
|
-
supersedes: number | null;
|
|
11
|
-
decidedBy: string;
|
|
12
|
-
decidedAt: string;
|
|
13
|
-
sourceKind: 'conversation' | 'task' | 'result' | 'manual' | 'backward-pass';
|
|
14
|
-
sourceRef: string | null;
|
|
15
|
-
recordedBy: string;
|
|
16
|
-
};
|
|
17
|
-
export type DecisionDraft = {
|
|
18
|
-
claim: string;
|
|
19
|
-
why: string;
|
|
20
|
-
decidedBy?: string;
|
|
21
|
-
decidedAt?: string;
|
|
22
|
-
sourceKind?: Decision['sourceKind'];
|
|
23
|
-
sourceRef?: string | null;
|
|
24
|
-
supersedes?: number | null;
|
|
25
|
-
};
|
|
26
5
|
export declare function listDecisions(store: Store, repo: string, actor: string, options?: {
|
|
27
6
|
status?: Decision['status'] | 'all';
|
|
28
7
|
limit?: number;
|
|
@@ -47,11 +26,6 @@ export declare function decisionHistory(store: Store, repo: string, actor: strin
|
|
|
47
26
|
at: string;
|
|
48
27
|
action: string;
|
|
49
28
|
}[];
|
|
50
|
-
export type DecisionLine = {
|
|
51
|
-
id: number;
|
|
52
|
-
claim: string;
|
|
53
|
-
decidedAt: string;
|
|
54
|
-
};
|
|
55
29
|
/**
|
|
56
30
|
* The decisions worth one line each in a brief: relevant to the query first,
|
|
57
31
|
* then newest, within a byte budget. Bodies (the why) load on demand by id,
|
package/dist/project-memory.js
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
import { createHash } from 'node:crypto';
|
|
11
11
|
import { scanForSecrets } from './evidence.js';
|
|
12
12
|
import { learningIdentity } from './project-learning.js';
|
|
13
|
+
import { TEXT_LIMITS } from './text-limits.js';
|
|
14
|
+
import { contractError } from './contracts/contract.js';
|
|
15
|
+
import { DECISION_CHANGE_VERSION, decisionChangeSchema, decisionRecordSchema, decisionSchema } from './contracts/project-memory.js';
|
|
13
16
|
export const MEMORY_SCHEMA = `
|
|
14
17
|
CREATE TABLE IF NOT EXISTS project_decision (
|
|
15
18
|
id INTEGER PRIMARY KEY, repo TEXT NOT NULL, identity TEXT NOT NULL, revision INTEGER NOT NULL DEFAULT 1,
|
|
@@ -75,18 +78,29 @@ function admission(store, repo, actor, write = false) {
|
|
|
75
78
|
throw Error('Project memory is outside your access.');
|
|
76
79
|
return learningIdentity(repo);
|
|
77
80
|
}
|
|
81
|
+
/** A decision row through its schema; null when the row does not read as one (it is then unverifiable). */
|
|
78
82
|
function decisionOf(row) {
|
|
79
|
-
|
|
83
|
+
const read = decisionSchema.safeParse({ id: Number(row['id']), repo: String(row['repo']), revision: Number(row['revision']), claim: String(row['claim']), why: String(row['why']),
|
|
80
84
|
status: row['status'], supersedes: row['supersedes'] === null ? null : Number(row['supersedes']),
|
|
81
85
|
decidedBy: String(row['decided_by']), decidedAt: String(row['decided_at']), sourceKind: row['source_kind'],
|
|
82
|
-
sourceRef: row['source_ref'] === null ? null : String(row['source_ref']), recordedBy: String(row['recorded_by']) };
|
|
86
|
+
sourceRef: row['source_ref'] === null ? null : String(row['source_ref']), recordedBy: String(row['recorded_by']) });
|
|
87
|
+
return read.success ? read.data : null;
|
|
83
88
|
}
|
|
84
89
|
const digestOf = (d) => sha(JSON.stringify([d.repo, d.revision, d.claim, d.why, d.status, d.supersedes, d.decidedBy, d.decidedAt, d.sourceKind, d.sourceRef, d.recordedBy]));
|
|
90
|
+
/** A record or history entry through its schema before it is written; a refusal names the field. */
|
|
91
|
+
function checked(schema, value) {
|
|
92
|
+
const read = schema.safeParse(value);
|
|
93
|
+
if (!read.success)
|
|
94
|
+
throw Error(`This decision could not be saved: ${contractError(read.error).join('; ')}`);
|
|
95
|
+
return read.data;
|
|
96
|
+
}
|
|
97
|
+
const history = (payload) => JSON.stringify(checked(decisionChangeSchema, { version: DECISION_CHANGE_VERSION, ...payload }));
|
|
85
98
|
export function listDecisions(store, repo, actor, options = {}) {
|
|
86
99
|
admission(store, repo, actor);
|
|
87
100
|
const status = options.status ?? 'active', limit = Math.min(200, Math.max(1, options.limit ?? 50));
|
|
88
101
|
return store.handle.prepare(`SELECT * FROM project_decision WHERE repo=? AND (?='all' OR status=?) ORDER BY id DESC LIMIT ?`).all(repo, status, status, limit)
|
|
89
|
-
.
|
|
102
|
+
.flatMap(row => { const d = decisionOf(row); return d === null ? [] : [d]; })
|
|
103
|
+
.filter(d => digestOf(d) === String(store.handle.prepare('SELECT sha FROM project_decision WHERE id=?').get(d.id)['sha']));
|
|
90
104
|
}
|
|
91
105
|
export function getDecision(store, repo, actor, id) {
|
|
92
106
|
admission(store, repo, actor);
|
|
@@ -94,20 +108,20 @@ export function getDecision(store, repo, actor, id) {
|
|
|
94
108
|
if (!row)
|
|
95
109
|
return null;
|
|
96
110
|
const d = decisionOf(row);
|
|
97
|
-
if (digestOf(d) !== String(row['sha']))
|
|
111
|
+
if (d === null || digestOf(d) !== String(row['sha']))
|
|
98
112
|
throw Error('This decision could not be verified.');
|
|
99
113
|
return d;
|
|
100
114
|
}
|
|
101
115
|
/** Record one settled choice. A superseding decision retires the older one in the same transaction. */
|
|
102
116
|
export function recordDecision(store, args, now = new Date()) {
|
|
103
117
|
const identity = admission(store, args.repo, args.actor, true);
|
|
104
|
-
const claim = clean(args.draft.claim,
|
|
105
|
-
const decidedBy = args.draft.decidedBy === undefined ? args.actor : clean(args.draft.decidedBy,
|
|
118
|
+
const claim = clean(args.draft.claim, TEXT_LIMITS.decisionClaimBytes, 'The decision'), why = clean(args.draft.why, TEXT_LIMITS.decisionWhyBytes, 'The reason');
|
|
119
|
+
const decidedBy = args.draft.decidedBy === undefined ? args.actor : clean(args.draft.decidedBy, TEXT_LIMITS.decisionByBytes, 'Who decided');
|
|
106
120
|
const decidedAt = args.draft.decidedAt ?? now.toISOString();
|
|
107
121
|
if (!/^\d{4}-\d{2}-\d{2}T/.test(decidedAt))
|
|
108
122
|
throw Error('When it was decided must be an ISO timestamp.');
|
|
109
123
|
const sourceKind = args.draft.sourceKind ?? 'manual';
|
|
110
|
-
const sourceRef = args.draft.sourceRef === undefined || args.draft.sourceRef === null ? null : clean(String(args.draft.sourceRef),
|
|
124
|
+
const sourceRef = args.draft.sourceRef === undefined || args.draft.sourceRef === null ? null : clean(String(args.draft.sourceRef), TEXT_LIMITS.decisionSourceBytes, 'The source');
|
|
111
125
|
return store.transact(() => {
|
|
112
126
|
let supersedes = null;
|
|
113
127
|
if (args.draft.supersedes !== undefined && args.draft.supersedes !== null) {
|
|
@@ -116,11 +130,11 @@ export function recordDecision(store, args, now = new Date()) {
|
|
|
116
130
|
throw Error('The decision being replaced is not active.');
|
|
117
131
|
supersedes = older.id;
|
|
118
132
|
}
|
|
119
|
-
const record = { repo: args.repo, revision: 1, claim, why, status: 'active', supersedes, decidedBy, decidedAt, sourceKind, sourceRef, recordedBy: args.actor };
|
|
133
|
+
const record = checked(decisionRecordSchema, { repo: args.repo, revision: 1, claim, why, status: 'active', supersedes, decidedBy, decidedAt, sourceKind, sourceRef, recordedBy: args.actor });
|
|
120
134
|
const inserted = store.handle.prepare(`INSERT INTO project_decision(repo,identity,revision,claim,why,status,supersedes,decided_by,decided_at,source_kind,source_ref,recorded_by,sha)
|
|
121
135
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(args.repo, identity, 1, claim, why, 'active', supersedes, decidedBy, decidedAt, sourceKind, sourceRef, args.actor, digestOf(record));
|
|
122
136
|
const id = Number(inserted.lastInsertRowid);
|
|
123
|
-
store.handle.prepare('INSERT INTO decision_change(decision,revision,actor,at,action,payload,sha) VALUES (?,?,?,?,?,?,?)').run(id, 1, args.actor, now.toISOString(), 'record',
|
|
137
|
+
store.handle.prepare('INSERT INTO decision_change(decision,revision,actor,at,action,payload,sha) VALUES (?,?,?,?,?,?,?)').run(id, 1, args.actor, now.toISOString(), 'record', history(record), digestOf(record));
|
|
124
138
|
if (supersedes !== null)
|
|
125
139
|
changeStatus(store, args.repo, args.actor, supersedes, 'superseded', `Replaced by decision ${id}.`, now);
|
|
126
140
|
reindexProjectMemory(store, args.repo);
|
|
@@ -133,15 +147,16 @@ function changeStatus(store, repo, actor, id, status, reason, now) {
|
|
|
133
147
|
throw Error('That decision is unavailable.');
|
|
134
148
|
if (existing.status !== 'active')
|
|
135
149
|
throw Error('That decision is no longer active.');
|
|
136
|
-
const
|
|
150
|
+
const { id: _id, ...was } = existing;
|
|
151
|
+
const next = { ...was, revision: existing.revision + 1, status };
|
|
137
152
|
store.handle.prepare('UPDATE project_decision SET revision=?,status=?,sha=? WHERE id=?').run(next.revision, status, digestOf(next), id);
|
|
138
|
-
store.handle.prepare('INSERT INTO decision_change(decision,revision,actor,at,action,payload,sha) VALUES (?,?,?,?,?,?,?)').run(id, next.revision, actor, now.toISOString(), status,
|
|
153
|
+
store.handle.prepare('INSERT INTO decision_change(decision,revision,actor,at,action,payload,sha) VALUES (?,?,?,?,?,?,?)').run(id, next.revision, actor, now.toISOString(), status, history({ reason }), digestOf(next));
|
|
139
154
|
return { id, ...next };
|
|
140
155
|
}
|
|
141
156
|
/** Retire a decision that no longer holds; history keeps it. */
|
|
142
157
|
export function retireDecision(store, args, now = new Date()) {
|
|
143
158
|
admission(store, args.repo, args.actor, true);
|
|
144
|
-
const reason = clean(args.reason,
|
|
159
|
+
const reason = clean(args.reason, TEXT_LIMITS.decisionRetireBytes, 'The reason');
|
|
145
160
|
return store.transact(() => { const d = changeStatus(store, args.repo, args.actor, args.id, 'retired', reason, now); reindexProjectMemory(store, args.repo); return d; });
|
|
146
161
|
}
|
|
147
162
|
export function decisionHistory(store, repo, actor, id) {
|
|
@@ -166,7 +181,7 @@ export function decisionLines(store, repo, query, options = {}) {
|
|
|
166
181
|
return { line: { id: Number(row['id']), claim, decidedAt: String(row['decided_at']) }, score };
|
|
167
182
|
})
|
|
168
183
|
.sort((a, b) => b.score - a.score || b.line.id - a.line.id);
|
|
169
|
-
const limit = options.limit ?? 8, bytes = options.bytes ??
|
|
184
|
+
const limit = options.limit ?? 8, bytes = options.bytes ?? TEXT_LIMITS.knowledgeDecisionsBytes, out = [];
|
|
170
185
|
for (const { line } of ranked) {
|
|
171
186
|
if (out.length >= limit)
|
|
172
187
|
break;
|
package/dist/project-skills.d.ts
CHANGED
|
@@ -1,48 +1,27 @@
|
|
|
1
1
|
import type { Store } from "./store.js";
|
|
2
|
+
import { SKILL_LIMITS, type SavedSkill, type SkillFile, type SkillPackage, type SkillSelection, type SkillsSnapshot } from "./contracts/project-skills.js";
|
|
3
|
+
export { SKILL_LIMITS };
|
|
4
|
+
export type { SavedSkill, SkillFile, SkillPackage, SkillsSnapshot };
|
|
2
5
|
export declare const SKILLS_SCHEMA: string;
|
|
3
|
-
|
|
4
|
-
path: string;
|
|
5
|
-
base64: string;
|
|
6
|
-
};
|
|
7
|
-
export type SkillPackage = {
|
|
8
|
-
name: string;
|
|
9
|
-
description: string;
|
|
10
|
-
source: string;
|
|
11
|
-
requirements: string;
|
|
12
|
-
warnings: string[];
|
|
13
|
-
files: SkillFile[];
|
|
14
|
-
};
|
|
15
|
-
export type SavedSkill = SkillPackage & {
|
|
16
|
-
sha: string;
|
|
17
|
-
};
|
|
18
|
-
type Choice = {
|
|
19
|
-
sha: string;
|
|
20
|
-
enabled: boolean;
|
|
21
|
-
};
|
|
22
|
-
type Selection = Record<string, Choice>;
|
|
23
|
-
export type SkillsSnapshot = {
|
|
24
|
-
version: 1;
|
|
25
|
-
revision: number;
|
|
26
|
-
identity: string;
|
|
27
|
-
inheritedFrom: number | null;
|
|
28
|
-
test: boolean;
|
|
29
|
-
packages: SavedSkill[];
|
|
30
|
-
};
|
|
31
|
-
export declare const SKILL_LIMITS: {
|
|
32
|
-
files: number;
|
|
33
|
-
packageBytes: number;
|
|
34
|
-
fileBytes: number;
|
|
35
|
-
bodyBytes: number;
|
|
36
|
-
enabled: number;
|
|
37
|
-
selectionBytes: number;
|
|
38
|
-
};
|
|
6
|
+
type Selection = SkillSelection;
|
|
39
7
|
export declare function validateSkill(files: SkillFile[], source: string): SkillPackage;
|
|
40
8
|
export declare function skillsView(store: Store, repo: string, actor: string): {
|
|
41
9
|
revision: number;
|
|
42
10
|
selection: Selection;
|
|
43
11
|
repo: string;
|
|
44
12
|
identity: string;
|
|
45
|
-
library:
|
|
13
|
+
library: {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
source: string;
|
|
17
|
+
requirements: string;
|
|
18
|
+
warnings: string[];
|
|
19
|
+
files: {
|
|
20
|
+
path: string;
|
|
21
|
+
base64: string;
|
|
22
|
+
}[];
|
|
23
|
+
sha: string;
|
|
24
|
+
}[];
|
|
46
25
|
history: {
|
|
47
26
|
revision: number;
|
|
48
27
|
actor: string;
|
|
@@ -159,4 +138,3 @@ export declare function githubSkill(url: string, fetcher?: typeof fetch): Promis
|
|
|
159
138
|
files: SkillFile[];
|
|
160
139
|
source: string;
|
|
161
140
|
}>;
|
|
162
|
-
export {};
|