@astrosheep/keiyaku 2.9.5 → 2.9.7
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/build/.tsbuildinfo +1 -1
- package/build/agents/harness/outcome.js +20 -4
- package/build/agents/harness/projection.js +8 -7
- package/build/agents/harness/router.js +2 -1
- package/build/agents/providers/claude-agent-sdk/adapter.js +13 -1
- package/build/agents/providers/claude-agent-sdk/registration.js +2 -1
- package/build/agents/providers/codex-app-server/adapter.js +11 -10
- package/build/agents/providers/codex-app-server/registration.js +2 -1
- package/build/agents/providers/opencode-sdk/adapter.js +7 -0
- package/build/agents/providers/opencode-sdk/registration.js +2 -1
- package/build/agents/providers/pi/adapter.js +10 -0
- package/build/agents/providers/pi/registration.js +3 -1
- package/build/cli/commands/contract/bind/handler.js +10 -4
- package/build/cli/commands/contract/bind/meta.js +6 -6
- package/build/cli/commands/contract/petition/handler.js +16 -3
- package/build/cli/commands/contract/petition/meta.js +4 -4
- package/build/cli/commands/metadata.js +3 -2
- package/build/cli/commands/projection/tell/handler.js +10 -2
- package/build/cli/commands/projection/tell/meta.js +3 -3
- package/build/cli/commands/task/catalog.js +2 -0
- package/build/cli/commands/task/log/handler.js +12 -0
- package/build/cli/commands/task/log/meta.js +8 -0
- package/build/cli/completion.js +9 -2
- package/build/cli/flags.js +11 -2
- package/build/cli/index.js +10 -6
- package/build/cli/parse-flags.js +6 -0
- package/build/cli/parse-metadata.js +1 -1
- package/build/cli/render/errors.js +18 -1
- package/build/cli/render/format.js +5 -2
- package/build/cli/render/line-width.js +33 -0
- package/build/cli/render/path-prefix-compaction.js +88 -0
- package/build/cli/render/petition.js +4 -0
- package/build/cli/render/projection-activity.js +93 -20
- package/build/cli/render/shared.js +34 -12
- package/build/cli/render/success-response.js +2 -0
- package/build/cli/render/tool-presentation.js +3 -3
- package/build/cli/render/wait.js +68 -48
- package/build/cli/subagent-guard.js +3 -0
- package/build/cli/types.js +1 -1
- package/build/core/audit/candidate.js +48 -0
- package/build/core/audit/coordinates.js +180 -0
- package/build/core/audit/evidence.js +158 -0
- package/build/core/audit/facade.js +42 -0
- package/build/core/audit/readiness.js +4 -0
- package/build/core/audit/report.js +38 -0
- package/build/core/audit.js +1 -650
- package/build/core/bind.js +111 -6
- package/build/core/draft.js +1 -1
- package/build/core/projection/generation/database.js +21 -2
- package/build/core/projection/generation/model.js +24 -3
- package/build/core/projection/generation/projection-generation-continuation.js +75 -10
- package/build/core/projection/generation/projection-generation-execution.js +4 -3
- package/build/core/projection/generation/projection-generation-runner.js +96 -53
- package/build/core/projection/generation/projection-generation-runtime.js +19 -0
- package/build/core/projection/generation/store.js +9 -0
- package/build/core/projection/generation/transitions.js +105 -3
- package/build/core/projection/index.js +2 -2
- package/build/core/projection/projection-core.js +1 -1
- package/build/core/projection/projection-kill.js +31 -0
- package/build/core/projection/projection-life-protocol.js +10 -0
- package/build/core/projection/projection-wait.js +53 -15
- package/build/core/projection/projection-wake.js +35 -3
- package/build/core/projection/tell/database.js +18 -0
- package/build/core/projection/tell/model.js +1 -0
- package/build/core/projection/tell/store.js +102 -55
- package/build/core/registry.js +82 -69
- package/build/core/scope.js +9 -9
- package/build/core/task/index.js +2 -2
- package/build/core/task/query.js +65 -23
- package/build/core/task/task-contract.js +18 -0
- package/build/core/task/task-git-store.js +50 -0
- package/build/generated/version.js +2 -2
- package/package.json +1 -1
- package/skills/keiyaku-akuma/SKILL.md +10 -0
- package/skills/keiyaku-workflow/SKILL.md +82 -7
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ProjectionStateError, randomHex8 } from "../../atomic-publish.js";
|
|
2
|
-
import { withRead, withWrite } from "./database.js";
|
|
3
|
-
import { assertTellIdRandomHex, originalFromRow, parseFence, requireExecutionId, submittedFromRow, } from "./model.js";
|
|
4
|
-
const PENDING_TELL_LIMIT = 10_000;
|
|
2
|
+
import { withRead, withReadSnapshot, withWrite } from "./database.js";
|
|
3
|
+
import { assertTellIdRandomHex, originalFromRow, PENDING_PROJECTION_TELL_STATES, parseFence, requireExecutionId, submittedFromRow, } from "./model.js";
|
|
5
4
|
function selectTell(database, tellId) {
|
|
6
5
|
return database.prepare(`
|
|
7
6
|
SELECT
|
|
@@ -26,6 +25,77 @@ export function listTellIds(projectionDirectory, state) {
|
|
|
26
25
|
ORDER BY tell_id
|
|
27
26
|
`).all(state).map((row) => row.tell_id));
|
|
28
27
|
}
|
|
28
|
+
/** List continuation fence metadata without loading operator tell bodies. */
|
|
29
|
+
export function listTellMetadataFromDatabase(database, states) {
|
|
30
|
+
const placeholders = states.map(() => "?").join(", ");
|
|
31
|
+
return database.prepare(`
|
|
32
|
+
SELECT tell_id, state, execution_id, effort
|
|
33
|
+
FROM projection_tells
|
|
34
|
+
WHERE state IN (${placeholders})
|
|
35
|
+
ORDER BY tell_id
|
|
36
|
+
`).all(...states).map((row) => ({
|
|
37
|
+
tellId: row.tell_id,
|
|
38
|
+
state: row.state,
|
|
39
|
+
...(row.execution_id ? { executionId: row.execution_id } : {}),
|
|
40
|
+
...(row.effort ? { effort: row.effort } : {}),
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
/** List continuation fence metadata without loading operator tell bodies. */
|
|
44
|
+
export function listTellMetadata(projectionDirectory, states) {
|
|
45
|
+
return withRead(projectionDirectory, (database) => listTellMetadataFromDatabase(database, states));
|
|
46
|
+
}
|
|
47
|
+
export function readPendingTellWindowFromDatabase(database, limit) {
|
|
48
|
+
if (!Number.isSafeInteger(limit) || limit < 0) {
|
|
49
|
+
throw new ProjectionStateError("pending tell window limit must be a non-negative safe integer");
|
|
50
|
+
}
|
|
51
|
+
const pendingPlaceholders = PENDING_PROJECTION_TELL_STATES.map(() => "?").join(", ");
|
|
52
|
+
const total = database.prepare(`
|
|
53
|
+
SELECT COUNT(*) AS count
|
|
54
|
+
FROM projection_tells
|
|
55
|
+
WHERE state IN (${pendingPlaceholders})
|
|
56
|
+
`).get(...PENDING_PROJECTION_TELL_STATES);
|
|
57
|
+
const rows = database.prepare(`
|
|
58
|
+
SELECT tell_id, text, created_at, state
|
|
59
|
+
FROM projection_tells
|
|
60
|
+
WHERE state IN (${pendingPlaceholders})
|
|
61
|
+
ORDER BY tell_id
|
|
62
|
+
LIMIT ?
|
|
63
|
+
`).all(...PENDING_PROJECTION_TELL_STATES, limit);
|
|
64
|
+
return {
|
|
65
|
+
totals: { undelivered: Number(total.count) },
|
|
66
|
+
window: rows.map((row) => ({ tellId: row.tell_id, text: row.text, createdAt: row.created_at, state: row.state })),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function readPendingTellWindow(projectionDirectory, limit) {
|
|
70
|
+
return withReadSnapshot(projectionDirectory, (database) => readPendingTellWindowFromDatabase(database, limit));
|
|
71
|
+
}
|
|
72
|
+
/** Whether a failed successor fence still retains delivered mail for replay. */
|
|
73
|
+
export function hasRetainedDeliveredTellFromDatabase(database, retainedFence) {
|
|
74
|
+
if (retainedFence.length === 0)
|
|
75
|
+
return false;
|
|
76
|
+
const statement = database.prepare(`
|
|
77
|
+
SELECT 1
|
|
78
|
+
FROM projection_tells
|
|
79
|
+
WHERE tell_id = ? AND state = 'delivered'
|
|
80
|
+
LIMIT 1
|
|
81
|
+
`);
|
|
82
|
+
return retainedFence.some((tellId) => statement.get(tellId) !== undefined);
|
|
83
|
+
}
|
|
84
|
+
export function hasRetainedDeliveredTell(projectionDirectory, retainedFence) {
|
|
85
|
+
return withRead(projectionDirectory, (database) => hasRetainedDeliveredTellFromDatabase(database, retainedFence));
|
|
86
|
+
}
|
|
87
|
+
/** Whether interrupted replay has any delivered tell left to return to inbox. */
|
|
88
|
+
export function hasDeliveredTellFromDatabase(database, executionId) {
|
|
89
|
+
return database.prepare(`
|
|
90
|
+
SELECT 1
|
|
91
|
+
FROM projection_tells
|
|
92
|
+
WHERE state = 'delivered' AND execution_id = ?
|
|
93
|
+
LIMIT 1
|
|
94
|
+
`).get(executionId) !== undefined;
|
|
95
|
+
}
|
|
96
|
+
export function hasDeliveredTell(projectionDirectory, executionId) {
|
|
97
|
+
return withRead(projectionDirectory, (database) => hasDeliveredTellFromDatabase(database, executionId));
|
|
98
|
+
}
|
|
29
99
|
export function readTellOriginal(projectionDirectory, state, tellId) {
|
|
30
100
|
return withRead(projectionDirectory, (database) => {
|
|
31
101
|
const row = selectTell(database, tellId);
|
|
@@ -49,14 +119,6 @@ export function writeInboxTell(projectionDirectory, text, nowMs, options) {
|
|
|
49
119
|
}
|
|
50
120
|
const tellId = `${nowMs}-${assertTellIdRandomHex((options?.randomHex ?? randomHex8)())}`;
|
|
51
121
|
return withWrite(projectionDirectory, (database) => {
|
|
52
|
-
const pending = database.prepare(`
|
|
53
|
-
SELECT COUNT(*) AS count
|
|
54
|
-
FROM projection_tells
|
|
55
|
-
WHERE state IN ('inbox', 'inflight', 'submitted', 'delivered')
|
|
56
|
-
`).get();
|
|
57
|
-
if (Number(pending.count) >= PENDING_TELL_LIMIT) {
|
|
58
|
-
throw new ProjectionStateError(`projection has reached the ${PENDING_TELL_LIMIT} pending tell limit`);
|
|
59
|
-
}
|
|
60
122
|
try {
|
|
61
123
|
database.prepare(`
|
|
62
124
|
INSERT INTO projection_tells (
|
|
@@ -123,6 +185,31 @@ export function claimFencedTells(projectionDirectory, fence, executionId) {
|
|
|
123
185
|
claimed.push(tellId);
|
|
124
186
|
continue;
|
|
125
187
|
}
|
|
188
|
+
if (row.state === "submitted" || row.state === "delivered") {
|
|
189
|
+
const submitted = submittedFromRow(row);
|
|
190
|
+
if (submitted.executionId === owner) {
|
|
191
|
+
throw new ProjectionStateError(`submitted tell belongs to current execution: ${tellId}`);
|
|
192
|
+
}
|
|
193
|
+
const replayedFrom = [
|
|
194
|
+
...(submitted.replayedFrom ?? []),
|
|
195
|
+
{ executionId: submitted.executionId, fence: submitted.fence },
|
|
196
|
+
];
|
|
197
|
+
const result = database.prepare(`
|
|
198
|
+
UPDATE projection_tells
|
|
199
|
+
SET
|
|
200
|
+
state = 'inflight',
|
|
201
|
+
execution_id = NULL,
|
|
202
|
+
fence_json = NULL,
|
|
203
|
+
replayed_from_json = ?,
|
|
204
|
+
delivered_at = NULL
|
|
205
|
+
WHERE tell_id = ? AND state = ?
|
|
206
|
+
`).run(JSON.stringify(replayedFrom), tellId, row.state);
|
|
207
|
+
if (Number(result.changes) !== 1) {
|
|
208
|
+
throw new ProjectionStateError(`generation tell fence member changed while claiming: ${tellId}`);
|
|
209
|
+
}
|
|
210
|
+
claimed.push(tellId);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
126
213
|
if (row.state === "consumed" && row.execution_id === owner)
|
|
127
214
|
continue;
|
|
128
215
|
throw new ProjectionStateError(`generation tell fence member is not claimable: ${tellId}`);
|
|
@@ -152,7 +239,7 @@ export function markTellSubmitted(projectionDirectory, tellId, fields) {
|
|
|
152
239
|
state = 'submitted',
|
|
153
240
|
execution_id = ?,
|
|
154
241
|
fence_json = ?,
|
|
155
|
-
replayed_from_json =
|
|
242
|
+
replayed_from_json = COALESCE(?, replayed_from_json)
|
|
156
243
|
WHERE tell_id = ? AND state = 'inflight'
|
|
157
244
|
`).run(executionId, JSON.stringify(fence), replayedFrom.length > 0 ? JSON.stringify(replayedFrom) : null, tellId);
|
|
158
245
|
});
|
|
@@ -190,12 +277,13 @@ export function readProjectionTellCounts(projectionDirectory) {
|
|
|
190
277
|
}
|
|
191
278
|
export function snapshotPendingTells(projectionDirectory) {
|
|
192
279
|
return withRead(projectionDirectory, (database) => {
|
|
280
|
+
const pendingPlaceholders = PENDING_PROJECTION_TELL_STATES.map(() => "?").join(", ");
|
|
193
281
|
const rows = database.prepare(`
|
|
194
282
|
SELECT tell_id, effort
|
|
195
283
|
FROM projection_tells
|
|
196
|
-
WHERE state IN (
|
|
284
|
+
WHERE state IN (${pendingPlaceholders})
|
|
197
285
|
ORDER BY tell_id
|
|
198
|
-
`).all();
|
|
286
|
+
`).all(...PENDING_PROJECTION_TELL_STATES);
|
|
199
287
|
const effort = rows.reduce((selected, row) => row.effort ?? selected, undefined);
|
|
200
288
|
return {
|
|
201
289
|
tellIds: rows.map((row) => row.tell_id),
|
|
@@ -203,44 +291,3 @@ export function snapshotPendingTells(projectionDirectory) {
|
|
|
203
291
|
};
|
|
204
292
|
});
|
|
205
293
|
}
|
|
206
|
-
export function demoteSubmittedTellsForReplay(projectionDirectory, executionId) {
|
|
207
|
-
const currentExecutionId = requireExecutionId(executionId);
|
|
208
|
-
return withWrite(projectionDirectory, (database) => {
|
|
209
|
-
const rows = database.prepare(`
|
|
210
|
-
SELECT
|
|
211
|
-
tell_id,
|
|
212
|
-
text,
|
|
213
|
-
created_at,
|
|
214
|
-
effort,
|
|
215
|
-
state,
|
|
216
|
-
execution_id,
|
|
217
|
-
fence_json,
|
|
218
|
-
replayed_from_json,
|
|
219
|
-
delivered_at
|
|
220
|
-
FROM projection_tells
|
|
221
|
-
WHERE state IN ('submitted', 'delivered')
|
|
222
|
-
ORDER BY tell_id
|
|
223
|
-
`).all();
|
|
224
|
-
for (const row of rows) {
|
|
225
|
-
const submitted = submittedFromRow(row);
|
|
226
|
-
if (submitted.executionId === currentExecutionId) {
|
|
227
|
-
throw new ProjectionStateError(`submitted tell belongs to current execution: ${row.tell_id}`);
|
|
228
|
-
}
|
|
229
|
-
const replayedFrom = [
|
|
230
|
-
...(submitted.replayedFrom ?? []),
|
|
231
|
-
{ executionId: submitted.executionId, fence: submitted.fence },
|
|
232
|
-
];
|
|
233
|
-
database.prepare(`
|
|
234
|
-
UPDATE projection_tells
|
|
235
|
-
SET
|
|
236
|
-
state = 'inflight',
|
|
237
|
-
execution_id = NULL,
|
|
238
|
-
fence_json = NULL,
|
|
239
|
-
replayed_from_json = ?,
|
|
240
|
-
delivered_at = NULL
|
|
241
|
-
WHERE tell_id = ? AND state IN ('submitted', 'delivered')
|
|
242
|
-
`).run(JSON.stringify(replayedFrom), row.tell_id);
|
|
243
|
-
}
|
|
244
|
-
return rows.map((row) => row.tell_id);
|
|
245
|
-
});
|
|
246
|
-
}
|
package/build/core/registry.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FlowError } from "../flow-error.js";
|
|
2
2
|
import { createGit, wrapGitError } from "../git/core.js";
|
|
3
3
|
import { readLedger } from "./ledger.js";
|
|
4
|
-
import { appendRefLogWithRetry } from "./ref-log.js";
|
|
4
|
+
import { appendRefLogWithRetry, readRefLog } from "./ref-log.js";
|
|
5
5
|
import { patternsOverlap, scopePathEvidence } from "./scope.js";
|
|
6
6
|
import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
|
|
7
7
|
export const REGISTRY_REF = "refs/keiyaku/registry";
|
|
@@ -39,11 +39,9 @@ async function activeRegistrations(cwd, entries) {
|
|
|
39
39
|
}
|
|
40
40
|
return active;
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
let overlaps = [];
|
|
44
|
-
let trackedPaths;
|
|
42
|
+
async function trackedRepositoryPaths(cwd) {
|
|
45
43
|
try {
|
|
46
|
-
|
|
44
|
+
return (await createGit(cwd).raw(["ls-files", "-z"]))
|
|
47
45
|
.split("\0")
|
|
48
46
|
.filter(Boolean)
|
|
49
47
|
.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
@@ -51,76 +49,91 @@ export async function registerScope(cwd, input) {
|
|
|
51
49
|
catch (error) {
|
|
52
50
|
throw wrapGitError("ls-files -z", error, cwd);
|
|
53
51
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
}
|
|
53
|
+
async function scopeRegistrationAdmission(cwd, input, entries, trackedPaths) {
|
|
54
|
+
const nextOverlaps = [];
|
|
55
|
+
const active = await activeRegistrations(cwd, entries);
|
|
56
|
+
if (input.exclusive && active.length > 0) {
|
|
57
|
+
const blocking = active.find((existing) => existing.contract !== input.contractId) ?? active[0];
|
|
58
|
+
throw new FlowError("ACTIVE_KEIYAKU_EXISTS", `exclusive bind rejected: active contract ${blocking.contract} already exists`);
|
|
59
|
+
}
|
|
60
|
+
for (const existing of active) {
|
|
61
|
+
if (existing.contract === input.contractId)
|
|
62
|
+
continue;
|
|
63
|
+
if (existing.tree === input.tree) {
|
|
64
|
+
throw new FlowError("ACTIVE_KEIYAKU_EXISTS", [
|
|
65
|
+
`tree is already occupied by active contract ${existing.contract}`,
|
|
66
|
+
`Run \`keiyaku forfeit @${existing.contract}\` to free this tree.`,
|
|
67
|
+
].join(" "));
|
|
60
68
|
}
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
const [nextEvidence, existingEvidence] = await Promise.all([
|
|
70
|
+
scopePathEvidence(cwd, input.scope, trackedPaths),
|
|
71
|
+
scopePathEvidence(cwd, existing.scope, trackedPaths),
|
|
72
|
+
]);
|
|
73
|
+
const existingByPath = new Map(existingEvidence.map((evidence) => [evidence.path, evidence]));
|
|
74
|
+
const concreteByPatterns = new Map();
|
|
75
|
+
for (const next of nextEvidence) {
|
|
76
|
+
const prior = existingByPath.get(next.path);
|
|
77
|
+
if (!next.matched || !prior?.matched || !next.decisivePattern || !prior.decisivePattern)
|
|
63
78
|
continue;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
]
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
for (const nextPattern of input.scope) {
|
|
103
|
-
for (const existingPattern of existing.scope) {
|
|
104
|
-
if (patternsOverlap(nextPattern, existingPattern)) {
|
|
105
|
-
const [nextPatternEvidence, existingPatternEvidence] = await Promise.all([
|
|
106
|
-
scopePathEvidence(cwd, [nextPattern], trackedPaths),
|
|
107
|
-
scopePathEvidence(cwd, [existingPattern], trackedPaths),
|
|
108
|
-
]);
|
|
109
|
-
const existingPatternByPath = new Map(existingPatternEvidence.map((evidence) => [evidence.path, evidence]));
|
|
110
|
-
const hasCurrentWitness = nextPatternEvidence.some((evidence) => (evidence.matched && existingPatternByPath.get(evidence.path)?.matched));
|
|
111
|
-
if (!hasCurrentWitness) {
|
|
112
|
-
nextOverlaps.push({
|
|
113
|
-
contracts: [input.contractId, existing.contract],
|
|
114
|
-
patterns: [nextPattern, existingPattern],
|
|
115
|
-
kind: "potential",
|
|
116
|
-
paths: { preview: [], count: 0, truncated: false },
|
|
117
|
-
});
|
|
118
|
-
}
|
|
79
|
+
const key = `${next.decisivePattern.index}:${prior.decisivePattern.index}`;
|
|
80
|
+
const group = concreteByPatterns.get(key) ?? {
|
|
81
|
+
next: next.decisivePattern.pattern,
|
|
82
|
+
existing: prior.decisivePattern.pattern,
|
|
83
|
+
paths: [],
|
|
84
|
+
};
|
|
85
|
+
group.paths.push(next.path);
|
|
86
|
+
concreteByPatterns.set(key, group);
|
|
87
|
+
}
|
|
88
|
+
for (const concrete of concreteByPatterns.values()) {
|
|
89
|
+
const preview = concrete.paths.slice(0, SCOPE_OVERLAP_PATH_PREVIEW_LIMIT);
|
|
90
|
+
nextOverlaps.push({
|
|
91
|
+
contracts: [input.contractId, existing.contract],
|
|
92
|
+
patterns: [concrete.next, concrete.existing],
|
|
93
|
+
kind: "concrete",
|
|
94
|
+
paths: {
|
|
95
|
+
preview,
|
|
96
|
+
count: concrete.paths.length,
|
|
97
|
+
truncated: preview.length < concrete.paths.length,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
for (const nextPattern of input.scope) {
|
|
102
|
+
for (const existingPattern of existing.scope) {
|
|
103
|
+
if (patternsOverlap(nextPattern, existingPattern)) {
|
|
104
|
+
const [nextPatternEvidence, existingPatternEvidence] = await Promise.all([
|
|
105
|
+
scopePathEvidence(cwd, [nextPattern], trackedPaths),
|
|
106
|
+
scopePathEvidence(cwd, [existingPattern], trackedPaths),
|
|
107
|
+
]);
|
|
108
|
+
const existingPatternByPath = new Map(existingPatternEvidence.map((evidence) => [evidence.path, evidence]));
|
|
109
|
+
const hasCurrentWitness = nextPatternEvidence.some((evidence) => (evidence.matched && existingPatternByPath.get(evidence.path)?.matched));
|
|
110
|
+
if (!hasCurrentWitness) {
|
|
111
|
+
nextOverlaps.push({
|
|
112
|
+
contracts: [input.contractId, existing.contract],
|
|
113
|
+
patterns: [nextPattern, existingPattern],
|
|
114
|
+
kind: "potential",
|
|
115
|
+
paths: { preview: [], count: 0, truncated: false },
|
|
116
|
+
});
|
|
119
117
|
}
|
|
120
118
|
}
|
|
121
119
|
}
|
|
122
120
|
}
|
|
123
|
-
|
|
121
|
+
}
|
|
122
|
+
return nextOverlaps;
|
|
123
|
+
}
|
|
124
|
+
/** Read-only bind admission: validate exclusivity and classify scope overlap. */
|
|
125
|
+
export async function previewScopeRegistration(cwd, input) {
|
|
126
|
+
const [entries, trackedPaths] = await Promise.all([
|
|
127
|
+
readRefLog(cwd, REGISTRY_REF, parseRegistryEntry),
|
|
128
|
+
trackedRepositoryPaths(cwd),
|
|
129
|
+
]);
|
|
130
|
+
return await scopeRegistrationAdmission(cwd, input, entries, trackedPaths);
|
|
131
|
+
}
|
|
132
|
+
export async function registerScope(cwd, input) {
|
|
133
|
+
let overlaps = [];
|
|
134
|
+
const trackedPaths = await trackedRepositoryPaths(cwd);
|
|
135
|
+
const result = await appendRefLogWithRetry(cwd, REGISTRY_REF, registrySubject, parseRegistryEntry, async (entries) => {
|
|
136
|
+
overlaps = await scopeRegistrationAdmission(cwd, input, entries, trackedPaths);
|
|
124
137
|
return {
|
|
125
138
|
v: 1,
|
|
126
139
|
kind: "register",
|
package/build/core/scope.js
CHANGED
|
@@ -38,17 +38,17 @@ export function isBlankScopePatternLine(line) {
|
|
|
38
38
|
}
|
|
39
39
|
/** Parse Keiyaku's source-line Scope opener, not a normalized Markdown node. */
|
|
40
40
|
export function parseScopeFenceOpening(line) {
|
|
41
|
-
const match = line.match(/^(`{3,})$/);
|
|
42
|
-
return match ? { length: match[1].length } : null;
|
|
41
|
+
const match = line.match(/^(`{3,}|~{3,})$/);
|
|
42
|
+
return match ? { delimiter: match[1][0], length: match[1].length } : null;
|
|
43
43
|
}
|
|
44
|
-
/** Keiyaku closer: column-zero
|
|
44
|
+
/** Keiyaku closer: matching column-zero delimiter, at least opener length, then whitespace. */
|
|
45
45
|
export function isScopeFenceClosing(line, fence) {
|
|
46
|
-
const match = line.match(/^(
|
|
47
|
-
return Boolean(match && match[1].length >= fence.length);
|
|
46
|
+
const match = line.match(/^(`+|~+)[ \t]*$/);
|
|
47
|
+
return Boolean(match && match[1][0] === fence.delimiter && match[1].length >= fence.length);
|
|
48
48
|
}
|
|
49
49
|
function fencedScopeError(heading, detail) {
|
|
50
50
|
return new FlowError("INVALID_KEIYAKU_DRAFT", `${heading} must contain exactly one fenced gitignore-subset block; ${detail}. ` +
|
|
51
|
-
`Paste-ready canonical example:\n\n${heading}\n
|
|
51
|
+
`Paste-ready canonical example:\n\n${heading}\n~~~\nfoo\nbar\n~~~`);
|
|
52
52
|
}
|
|
53
53
|
/** Validate and normalize the one fenced block that constitutes a Scope body. */
|
|
54
54
|
export function validateScopeFragmentLines(lines, options) {
|
|
@@ -152,14 +152,14 @@ export function markdownSourceSections(markdown) {
|
|
|
152
152
|
return { ...heading, end: next?.start ?? markdown.length };
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
|
-
/** Render canonical Scope bytes with a fence that content cannot close. */
|
|
155
|
+
/** Render canonical Scope bytes with a tilde fence that content cannot close. */
|
|
156
156
|
export function renderScopeFragment(patterns) {
|
|
157
157
|
let longestRun = 0;
|
|
158
158
|
for (const pattern of patterns) {
|
|
159
|
-
for (const run of pattern.match(
|
|
159
|
+
for (const run of pattern.match(/~+/g) ?? [])
|
|
160
160
|
longestRun = Math.max(longestRun, run.length);
|
|
161
161
|
}
|
|
162
|
-
const fence = "
|
|
162
|
+
const fence = "~".repeat(Math.max(3, longestRun + 1));
|
|
163
163
|
return `${fence}\n${patterns.join("\n")}\n${fence}`;
|
|
164
164
|
}
|
|
165
165
|
/**
|
package/build/core/task/index.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Only symbols required by current external consumers are re-exported.
|
|
4
4
|
* Package-internal modules must import each other directly, not through this file.
|
|
5
5
|
*/
|
|
6
|
-
export { reconcileTaskForfeit, settleTaskBoardForBind, settleTaskBoardForLanding, } from "./task-contract.js";
|
|
6
|
+
export { previewTaskBoardForBind, reconcileTaskForfeit, settleTaskBoardForBind, settleTaskBoardForLanding, } from "./task-contract.js";
|
|
7
7
|
export { resolveTaskCreator } from "./task-git-actor.js";
|
|
8
8
|
export { openTaskCommandRepository, readTaskDoctor, taskCommandContractId, } from "./task-git-runtime.js";
|
|
9
|
-
export { openTaskStore } from "./task-git-store.js";
|
|
9
|
+
export { openTaskStore, readTaskHistory } from "./task-git-store.js";
|
|
10
10
|
export { taskRepositoryFromStore } from "./task-store-repository.js";
|
|
11
11
|
export { isTaskId } from "./validation.js";
|
|
12
12
|
export { buildTaskTree, createTask, doneTask, dropTask, parseTaskDocument, readTaskBoard, resolveTaskReadId, serializeTaskDocument, startTask, stopTask, updateTask, deriveTaskDisplay, selectTaskList, taskUpdatedAt, advancedTaskUpdatedAt, } from "./task.js";
|
package/build/core/task/query.js
CHANGED
|
@@ -2,10 +2,16 @@ import { FlowError } from "../../flow-error.js";
|
|
|
2
2
|
import { isCommissionId } from "../ids.js";
|
|
3
3
|
import { taskUpdatedAt } from "./model.js";
|
|
4
4
|
import { isTaskId } from "./validation.js";
|
|
5
|
-
const
|
|
6
|
-
"state", "priority", "parent", "needs", "blocks", "contract", "created", "updated",
|
|
7
|
-
]
|
|
5
|
+
export const TASK_FILTER_FIELDS = [
|
|
6
|
+
"title", "state", "priority", "parent", "needs", "blocks", "contract", "created", "updated",
|
|
7
|
+
];
|
|
8
|
+
export const TASK_FILTER_OPERATORS = ["=", "!=", "~", "<", "<=", ">", ">="];
|
|
9
|
+
export const TASK_FILTER_GRAMMAR = "FIELD OP VALUE[,VALUE...] [| FIELD OP VALUE[,VALUE...]]...";
|
|
10
|
+
export const TASK_FILTER_COMPLETION = "FIELD OP VALUE[,VALUE...]|FIELD OP VALUE[,VALUE...]";
|
|
11
|
+
const FILTER_FIELDS = new Set(TASK_FILTER_FIELDS);
|
|
8
12
|
const ORDERED_FIELDS = new Set(["priority", "created", "updated"]);
|
|
13
|
+
const TITLE_OPERATORS = new Set(["=", "!=", "~"]);
|
|
14
|
+
const ORDERED_OPERATORS = new Set(["=", "!=", "<", "<=", ">", ">="]);
|
|
9
15
|
const DISPLAY_VALUES = new Set(["ready", "blocked", "in_progress", "done", "dropped", "bound"]);
|
|
10
16
|
const SORT_KEYS = new Set(["priority", "modified", "created"]);
|
|
11
17
|
const RELATIVE_TIME = /^now-([1-9][0-9]*)([smhdw])$/;
|
|
@@ -21,57 +27,87 @@ export function deriveTaskDisplay(task) {
|
|
|
21
27
|
return "in_progress";
|
|
22
28
|
return task.blocked ? "blocked" : "ready";
|
|
23
29
|
}
|
|
24
|
-
function invalidFilter(detail) {
|
|
25
|
-
throw new FlowError("INVALID_TARGET", `invalid task filter: ${detail}
|
|
30
|
+
function invalidFilter(detail, recoveredTerms = []) {
|
|
31
|
+
throw new FlowError("INVALID_TARGET", `invalid task filter: ${detail}`, {
|
|
32
|
+
facts: {
|
|
33
|
+
kind: "invalid_task_filter",
|
|
34
|
+
detail,
|
|
35
|
+
grammar: TASK_FILTER_GRAMMAR,
|
|
36
|
+
fields: [...TASK_FILTER_FIELDS],
|
|
37
|
+
operators: [...TASK_FILTER_OPERATORS],
|
|
38
|
+
recoveredTerms: recoveredTerms.map((term) => ({
|
|
39
|
+
field: term.field,
|
|
40
|
+
operator: term.operator,
|
|
41
|
+
values: [...term.values],
|
|
42
|
+
})),
|
|
43
|
+
},
|
|
44
|
+
});
|
|
26
45
|
}
|
|
27
|
-
|
|
28
|
-
const match = /^([a-z][a-z0-9_-]*)(
|
|
46
|
+
function parseTaskFilterTerm(term, expression, recoveredTerms) {
|
|
47
|
+
const match = /^([a-z][a-z0-9_-]*)(!=|<=|>=|~|=|<|>)(.*)$/.exec(term);
|
|
29
48
|
if (!match)
|
|
30
|
-
invalidFilter(`'${expression}' must use
|
|
49
|
+
invalidFilter(`'${expression}' must use ${TASK_FILTER_GRAMMAR}`, recoveredTerms);
|
|
31
50
|
const field = match[1];
|
|
32
51
|
const operator = match[2];
|
|
33
52
|
if (!FILTER_FIELDS.has(field))
|
|
34
|
-
invalidFilter(`unknown field '${field}'
|
|
35
|
-
if (
|
|
36
|
-
invalidFilter(`field '${field}' does not support operator '${operator}'
|
|
53
|
+
invalidFilter(`unknown field '${field}'`, recoveredTerms);
|
|
54
|
+
if (field === "title" && !TITLE_OPERATORS.has(operator)) {
|
|
55
|
+
invalidFilter(`field '${field}' does not support operator '${operator}'`, recoveredTerms);
|
|
56
|
+
}
|
|
57
|
+
if (field !== "title" && (ORDERED_FIELDS.has(field)
|
|
58
|
+
? !ORDERED_OPERATORS.has(operator)
|
|
59
|
+
: operator !== "=" && operator !== "!=")) {
|
|
60
|
+
invalidFilter(`field '${field}' does not support operator '${operator}'`, recoveredTerms);
|
|
37
61
|
}
|
|
38
62
|
const values = match[3].split(",");
|
|
39
63
|
if (values.some((value) => value.length === 0))
|
|
40
|
-
invalidFilter(`field '${field}' has an empty value
|
|
41
|
-
validateFilterValues(field, values);
|
|
64
|
+
invalidFilter(`field '${field}' has an empty value`, recoveredTerms);
|
|
65
|
+
validateFilterValues(field, values, recoveredTerms);
|
|
42
66
|
return { field, operator, values };
|
|
43
67
|
}
|
|
44
|
-
function
|
|
68
|
+
export function parseTaskFilter(expression) {
|
|
69
|
+
const terms = expression.split("|");
|
|
70
|
+
const parsed = [];
|
|
71
|
+
for (const term of terms) {
|
|
72
|
+
if (term.length === 0)
|
|
73
|
+
invalidFilter(`'${expression}' must use ${TASK_FILTER_GRAMMAR}`, parsed);
|
|
74
|
+
parsed.push(parseTaskFilterTerm(term, expression, parsed));
|
|
75
|
+
}
|
|
76
|
+
return parsed;
|
|
77
|
+
}
|
|
78
|
+
function parseTimestamp(value, nowMs, recoveredTerms = []) {
|
|
45
79
|
const relative = RELATIVE_TIME.exec(value);
|
|
46
80
|
if (relative) {
|
|
47
81
|
const amount = Number(relative[1]);
|
|
48
82
|
const offset = amount * UNIT_MS[relative[2]];
|
|
49
83
|
if (!Number.isSafeInteger(offset))
|
|
50
|
-
invalidFilter(`timestamp '${value}' is outside the supported range
|
|
84
|
+
invalidFilter(`timestamp '${value}' is outside the supported range`, recoveredTerms);
|
|
51
85
|
return nowMs - offset;
|
|
52
86
|
}
|
|
53
87
|
const timestamp = Date.parse(value);
|
|
54
88
|
if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString() !== value) {
|
|
55
|
-
invalidFilter(`timestamp '${value}' must be canonical ISO-8601 or now-<duration
|
|
89
|
+
invalidFilter(`timestamp '${value}' must be canonical ISO-8601 or now-<duration>`, recoveredTerms);
|
|
56
90
|
}
|
|
57
91
|
return timestamp;
|
|
58
92
|
}
|
|
59
|
-
function validateFilterValues(field, values) {
|
|
93
|
+
function validateFilterValues(field, values, recoveredTerms) {
|
|
94
|
+
if (field === "title")
|
|
95
|
+
return;
|
|
60
96
|
if (field === "state") {
|
|
61
97
|
for (const value of values)
|
|
62
98
|
if (!DISPLAY_VALUES.has(value))
|
|
63
|
-
invalidFilter(`unknown state '${value}'
|
|
99
|
+
invalidFilter(`unknown state '${value}'`, recoveredTerms);
|
|
64
100
|
return;
|
|
65
101
|
}
|
|
66
102
|
if (field === "priority") {
|
|
67
103
|
for (const value of values)
|
|
68
104
|
if (!/^[0-3]$/.test(value))
|
|
69
|
-
invalidFilter(`priority '${value}' must be 0 through 3
|
|
105
|
+
invalidFilter(`priority '${value}' must be 0 through 3`, recoveredTerms);
|
|
70
106
|
return;
|
|
71
107
|
}
|
|
72
108
|
if (field === "created" || field === "updated") {
|
|
73
109
|
for (const value of values)
|
|
74
|
-
parseTimestamp(value, 0);
|
|
110
|
+
parseTimestamp(value, 0, recoveredTerms);
|
|
75
111
|
return;
|
|
76
112
|
}
|
|
77
113
|
if (field === "parent" || field === "needs" || field === "blocks") {
|
|
@@ -79,14 +115,14 @@ function validateFilterValues(field, values) {
|
|
|
79
115
|
if (field === "parent" && value === "none")
|
|
80
116
|
continue;
|
|
81
117
|
if (!isTaskId(value))
|
|
82
|
-
invalidFilter(`value '${value}' must be a canonical full task id
|
|
118
|
+
invalidFilter(`value '${value}' must be a canonical full task id`, recoveredTerms);
|
|
83
119
|
}
|
|
84
120
|
return;
|
|
85
121
|
}
|
|
86
122
|
if (field === "contract") {
|
|
87
123
|
for (const value of values) {
|
|
88
124
|
if (value !== "none" && !isCommissionId(value))
|
|
89
|
-
invalidFilter(`value '${value}' must be a full commission id or none
|
|
125
|
+
invalidFilter(`value '${value}' must be a full commission id or none`, recoveredTerms);
|
|
90
126
|
}
|
|
91
127
|
}
|
|
92
128
|
}
|
|
@@ -114,10 +150,16 @@ function compareValue(left, right, operator) {
|
|
|
114
150
|
case "<=": return left <= right;
|
|
115
151
|
case ">": return left > right;
|
|
116
152
|
case ">=": return left >= right;
|
|
153
|
+
case "~": return false;
|
|
117
154
|
}
|
|
118
155
|
}
|
|
119
156
|
function matchesFilter(task, filter, nowMs) {
|
|
120
157
|
return filter.values.some((raw) => {
|
|
158
|
+
if (filter.field === "title") {
|
|
159
|
+
if (filter.operator === "~")
|
|
160
|
+
return task.title.toLowerCase().includes(raw.toLowerCase());
|
|
161
|
+
return compareValue(task.title, raw, filter.operator);
|
|
162
|
+
}
|
|
121
163
|
if (filter.field === "state")
|
|
122
164
|
return compareValue(deriveTaskDisplay(task), raw, filter.operator);
|
|
123
165
|
if (filter.field === "priority")
|
|
@@ -167,6 +209,6 @@ export function selectTaskList(tasks, options = {}) {
|
|
|
167
209
|
const nowMs = options.nowMs ?? Date.now();
|
|
168
210
|
return tasks
|
|
169
211
|
.filter((task) => selected(task, options.selection ?? "default"))
|
|
170
|
-
.filter((task) => filters.every((filter) => matchesFilter(task,
|
|
212
|
+
.filter((task) => filters.every((filter) => filter.some((term) => matchesFilter(task, term, nowMs))))
|
|
171
213
|
.sort(sort === undefined ? defaultCompare : (left, right) => explicitCompare(sort, left, right));
|
|
172
214
|
}
|
|
@@ -7,6 +7,24 @@ import { buildTaskSourceBoard } from "./source-board.js";
|
|
|
7
7
|
import { composeLandingTree, containsCommit, createLandingCommit, createTaskBoardCommit, findAcceptedLandingCommit, publishTaskMutationCommit, inspectCurrentBranch, publishForwardTaskBoardCommit, reconcileTaskPathsetIndex, refHead, taskContractAtCommit, taskSettlementOwnershipAtCommit, taskSourcesAtCommit, taskWritesFromSources, trackedTaskPaths, updateBranch, updateTargetRef, } from "./settlement-git.js";
|
|
8
8
|
import { contractPointerDocument, doneTaskReplacements, reopenedTaskDocument, requireHealthySettlementBoard, requireSettledTask, selectReadyTasksForBind, tasksForContract, } from "./settlement-policy.js";
|
|
9
9
|
import { stableRepoRoot } from "../worktree-path.js";
|
|
10
|
+
/**
|
|
11
|
+
* Read the same healthy task board and run the same ready-task selector as bind
|
|
12
|
+
* under the task admission lock, without replacing sources or publishing.
|
|
13
|
+
*/
|
|
14
|
+
export async function previewTaskBoardForBind(input) {
|
|
15
|
+
await requireGitRepository(input.cwd, "task bind");
|
|
16
|
+
return await withLockHeldTaskBoard(input.cwd, async (transaction) => {
|
|
17
|
+
if (transaction.location.mode !== "git") {
|
|
18
|
+
if (transaction.location.capability === "unavailable") {
|
|
19
|
+
assertGitObservation({ capability: "unavailable" }, "task bind");
|
|
20
|
+
}
|
|
21
|
+
throw new FlowError("NOT_GIT_REPO", "task bind requires Git task storage");
|
|
22
|
+
}
|
|
23
|
+
const repository = taskRepositoryFromLockedBoard(transaction);
|
|
24
|
+
const board = requireHealthySettlementBoard(await repository.read());
|
|
25
|
+
return { tasks: input.taskIds === undefined ? [] : selectReadyTasksForBind(board, input.taskIds) };
|
|
26
|
+
});
|
|
27
|
+
}
|
|
10
28
|
function sourcePath(root, source) {
|
|
11
29
|
return source.path.slice(root.length + 1).split("\\").join("/");
|
|
12
30
|
}
|