@method-ai-hq/sdk 0.18.2 → 0.20.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/README.md +1 -1
- package/dist/packages/sdk/package.json +7 -2
- package/dist/packages/sdk/src/authoring-instructions.d.ts +1 -1
- package/dist/packages/sdk/src/authoring-instructions.js +1 -1
- package/dist/packages/sdk/src/current-inspection.js +3 -0
- package/dist/packages/sdk/src/current-runtime.d.ts +6 -1
- package/dist/packages/sdk/src/current-runtime.js +47 -2
- package/dist/packages/sdk/src/fix-lines.d.ts +82 -0
- package/dist/packages/sdk/src/fix-lines.js +63 -0
- package/dist/packages/sdk/src/fix-outcomes.d.ts +72 -0
- package/dist/packages/sdk/src/fix-outcomes.js +201 -0
- package/dist/packages/sdk/src/home.d.ts +30 -26
- package/dist/packages/sdk/src/home.js +164 -60
- package/dist/packages/sdk/src/improve.d.ts +146 -24
- package/dist/packages/sdk/src/improve.js +665 -105
- package/dist/packages/sdk/src/judge.d.ts +23 -10
- package/dist/packages/sdk/src/judge.js +110 -26
- package/dist/packages/sdk/src/learned.d.ts +43 -0
- package/dist/packages/sdk/src/learned.js +97 -0
- package/dist/packages/sdk/src/local-cli.js +8 -3
- package/dist/packages/sdk/src/local-judgments.d.ts +2 -0
- package/dist/packages/sdk/src/merge.d.ts +38 -0
- package/dist/packages/sdk/src/merge.js +65 -0
- package/dist/packages/sdk/src/method-client.js +1 -1
- package/dist/packages/sdk/src/method-files.d.ts +3 -2
- package/dist/packages/sdk/src/method-files.js +7 -3
- package/dist/packages/sdk/src/method-help.d.ts +1 -1
- package/dist/packages/sdk/src/method-help.js +25 -16
- package/dist/packages/sdk/src/method-sync.d.ts +7 -0
- package/dist/packages/sdk/src/method-sync.js +33 -0
- package/dist/packages/sdk/src/method.d.ts +5 -0
- package/dist/packages/sdk/src/method.js +169 -15
- package/dist/packages/sdk/src/outbox.d.ts +28 -6
- package/dist/packages/sdk/src/outbox.js +13 -6
- package/dist/packages/sdk/src/proposals.d.ts +0 -28
- package/dist/packages/sdk/src/proposals.js +2 -129
- package/dist/packages/sdk/src/quality.d.ts +65 -5
- package/dist/packages/sdk/src/quality.js +82 -17
- package/dist/packages/sdk/src/run-saved.d.ts +4 -0
- package/dist/packages/sdk/src/run-saved.js +33 -2
- package/dist/packages/sdk/src/undo.d.ts +50 -0
- package/dist/packages/sdk/src/undo.js +108 -0
- package/dist/packages/sdk/src/versions.js +5 -3
- package/dist/packages/sdk/src/watch.d.ts +20 -0
- package/dist/packages/sdk/src/watch.js +86 -0
- package/dist/packages/workflow-language/src/inspection.d.ts +30 -0
- package/dist/packages/workflow-language/src/inspection.js +3 -1
- package/package.json +7 -2
|
@@ -1,46 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Each Method has one home file on this computer: the file its versions are saved from. A copy in a temporary folder,
|
|
3
3
|
* or in another folder while the home file still exists, is not saved, so one Method never grows two histories.
|
|
4
|
-
* Git worktrees and clones of one repository share a home.
|
|
5
|
-
* wrote, so an unchanged home file can take a newer version from the account.
|
|
4
|
+
* Git worktrees and clones of one repository share a home.
|
|
6
5
|
*/
|
|
7
6
|
export type Home = {
|
|
8
7
|
path: string;
|
|
9
8
|
key: string;
|
|
10
|
-
version_id?: string;
|
|
11
|
-
files?: Record<string, string>;
|
|
12
9
|
};
|
|
13
10
|
export declare function readHome(server: string, methodId: string): Home | undefined;
|
|
14
11
|
export declare const temporary: (path: string) => boolean;
|
|
15
12
|
/** The same file in every checkout of one repository has the same key. */
|
|
16
13
|
export declare function homeKey(file: string): string;
|
|
17
|
-
|
|
18
|
-
export declare function writtenFiles(file: string, packageFiles: Array<{
|
|
19
|
-
path: string;
|
|
20
|
-
sha256: string;
|
|
21
|
-
}>): {
|
|
22
|
-
[x: string]: string;
|
|
23
|
-
};
|
|
24
|
-
export declare function recordHome(server: string, methodId: string, file: string, version?: {
|
|
25
|
-
version_id: string;
|
|
26
|
-
files: Record<string, string>;
|
|
27
|
-
}): void;
|
|
14
|
+
export declare function recordHome(server: string, methodId: string, file: string): void;
|
|
28
15
|
/**
|
|
29
16
|
* Throws, with the home file named, when this file may not save the Method. Otherwise this file is the home after
|
|
30
17
|
* the call. A home that is gone, holds another Method, or is a temporary copy gives way to a file outside a
|
|
31
18
|
* temporary folder.
|
|
32
19
|
*/
|
|
33
20
|
export declare function claimHome(server: string, methodId: string, file: string): void;
|
|
34
|
-
|
|
35
|
-
|
|
21
|
+
type Client = Pick<import('./method-client.js').MethodClient, 'server' | 'token' | 'request' | 'transfer'>;
|
|
22
|
+
/** Both texts of one part that this computer and a teammate changed in different ways. */
|
|
23
|
+
export type CombineConflict = {
|
|
24
|
+
part: string;
|
|
25
|
+
file: string;
|
|
26
|
+
yours: unknown;
|
|
27
|
+
theirs: unknown;
|
|
28
|
+
};
|
|
29
|
+
export type CatchUp = {
|
|
30
|
+
status: 'skipped' | 'current';
|
|
31
|
+
} | {
|
|
32
|
+
status: 'offline' | 'updated';
|
|
33
|
+
line: string;
|
|
34
|
+
} | {
|
|
35
|
+
status: 'conflict';
|
|
36
|
+
result: {
|
|
37
|
+
status: 'needs_you';
|
|
38
|
+
failure: import('../../workflow-language/src/inspection.js').RunFailure;
|
|
39
|
+
conflicts: CombineConflict[];
|
|
40
|
+
};
|
|
41
|
+
};
|
|
36
42
|
/**
|
|
37
|
-
* Before a run of
|
|
38
|
-
*
|
|
39
|
-
*
|
|
43
|
+
* Before a run, a fix or a publish of a Method file: bring the file to the newest version of its Method. base is the
|
|
44
|
+
* version this computer last saved or got, ours the file, theirs the newest version. An unchanged file takes theirs;
|
|
45
|
+
* a changed one combines step by step and file by file (an empty base when this computer has none). A part that both
|
|
46
|
+
* sides changed in different ways is a conflict: nothing changes, and the result has both texts. Offline, the file
|
|
47
|
+
* runs as it is. Signed out, a file with no id: line, or a Method with no version yet: nothing.
|
|
40
48
|
*/
|
|
41
|
-
export declare function
|
|
42
|
-
|
|
43
|
-
token(): string | null | undefined;
|
|
44
|
-
request<T>(path: string): Promise<T>;
|
|
45
|
-
transfer(path: string): Promise<Uint8Array>;
|
|
46
|
-
}, file: string): Promise<boolean>;
|
|
49
|
+
export declare function catchUp(client: Client, file: string, doing?: string): Promise<CatchUp>;
|
|
50
|
+
export {};
|
|
@@ -43,13 +43,8 @@ export function homeKey(file) {
|
|
|
43
43
|
const path = real(authoringPath(file)), repo = repository(path);
|
|
44
44
|
return repo ? `${repo.main}:${relative(repo.root, path)}` : path;
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const path = authoringPath(file);
|
|
49
|
-
return { [relative(dirname(path), path)]: fileHash(readFileSync(path)), ...Object.fromEntries(packageFiles.map(f => [f.path, f.sha256])) };
|
|
50
|
-
}
|
|
51
|
-
export function recordHome(server, methodId, file, version) {
|
|
52
|
-
writePrivateJson(homeFile(server, methodId), { path: authoringPath(file), key: homeKey(file), ...version });
|
|
46
|
+
export function recordHome(server, methodId, file) {
|
|
47
|
+
writePrivateJson(homeFile(server, methodId), { path: authoringPath(file), key: homeKey(file) });
|
|
53
48
|
}
|
|
54
49
|
/**
|
|
55
50
|
* Throws, with the home file named, when this file may not save the Method. Otherwise this file is the home after
|
|
@@ -60,7 +55,7 @@ export function claimHome(server, methodId, file) {
|
|
|
60
55
|
const home = readHome(server, methodId), key = homeKey(file);
|
|
61
56
|
if (home && home.key === key) {
|
|
62
57
|
if (home.path !== authoringPath(file))
|
|
63
|
-
recordHome(server, methodId, file
|
|
58
|
+
recordHome(server, methodId, file);
|
|
64
59
|
return;
|
|
65
60
|
}
|
|
66
61
|
const homeLives = (() => { try {
|
|
@@ -73,65 +68,174 @@ export function claimHome(server, methodId, file) {
|
|
|
73
68
|
throw Object.assign(Error(`This Method is saved from ${home.path}. Make the change in that file and run it there; this copy was not run or saved.`), { code: 'other_home', home: home.path });
|
|
74
69
|
recordHome(server, methodId, file);
|
|
75
70
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return Object.entries(home.files).every(([name, hash]) => { try {
|
|
82
|
-
return fileHash(readFileSync(join(root, name))) === hash;
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
return false;
|
|
86
|
-
} });
|
|
87
|
-
}
|
|
71
|
+
const list = (word, items) => items.length ? `${word}${items.length === 1 ? '' : 's'} ${items.join(', ')}` : '';
|
|
72
|
+
const parts = (steps, files) => [list('step', steps), list('file', files)].filter(Boolean).join(' and ');
|
|
73
|
+
const clock = (at) => { const date = at ? new Date(at) : undefined; return date && !isNaN(+date) ? ` from ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}` : ''; };
|
|
74
|
+
const clip = (text) => text.length > 20_000 ? `${text.slice(0, 20_000)}…` : text;
|
|
75
|
+
const unreachable = (error) => error?.code === 'network' || error?.code === 'stalled' || error?.name === 'TimeoutError' || error?.name === 'AbortError' || error?.status >= 500 || error?.status === 429;
|
|
88
76
|
/**
|
|
89
|
-
* Before a run of
|
|
90
|
-
*
|
|
91
|
-
*
|
|
77
|
+
* Before a run, a fix or a publish of a Method file: bring the file to the newest version of its Method. base is the
|
|
78
|
+
* version this computer last saved or got, ours the file, theirs the newest version. An unchanged file takes theirs;
|
|
79
|
+
* a changed one combines step by step and file by file (an empty base when this computer has none). A part that both
|
|
80
|
+
* sides changed in different ways is a conflict: nothing changes, and the result has both texts. Offline, the file
|
|
81
|
+
* runs as it is. Signed out, a file with no id: line, or a Method with no version yet: nothing.
|
|
92
82
|
*/
|
|
93
|
-
export async function
|
|
94
|
-
const path = authoringPath(file);
|
|
83
|
+
export async function catchUp(client, file, doing = 'running') {
|
|
84
|
+
const path = authoringPath(file), root = dirname(path);
|
|
85
|
+
const { methodContent, methodIdPattern, versionIdFor } = await import('./versions.js');
|
|
86
|
+
let document;
|
|
87
|
+
try {
|
|
88
|
+
document = readDocument(path);
|
|
89
|
+
if (typeof document.id !== 'string' || !methodIdPattern.test(document.id) || !client.token())
|
|
90
|
+
return { status: 'skipped' };
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return { status: 'skipped' };
|
|
94
|
+
}
|
|
95
|
+
const id = document.id, options = { signal: AbortSignal.timeout(5000), stallMs: 5000 };
|
|
96
|
+
const methodPath = `/api/cli/methods/${encodeURIComponent(id)}`;
|
|
97
|
+
const { lastSaved, recordCheckout } = await import('./outbox.js');
|
|
98
|
+
const record = lastSaved(client.server, id);
|
|
99
|
+
// merge_base: the newest version that the server has as an ancestor of both this computer's version and the newest one.
|
|
100
|
+
let theirs;
|
|
101
|
+
try {
|
|
102
|
+
theirs = await client.request(record?.version_id ? `${methodPath}?base=${encodeURIComponent(record.version_id)}` : methodPath, 'GET', undefined, true, options);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
return unreachable(error) ? { status: 'offline', line: `Could not check for a newer version; ${doing} this file.` } : { status: 'skipped' };
|
|
106
|
+
}
|
|
107
|
+
const { loadWorkflow } = await import('../../workflow-language/src/validate.js');
|
|
108
|
+
const { buildPackage } = await import('./method-files.js');
|
|
109
|
+
// A file that does not load fails its run with its own error.
|
|
110
|
+
let ours;
|
|
95
111
|
try {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
const workflow = loadWorkflow(methodContent(document)), { pack } = await buildPackage(path, workflow);
|
|
113
|
+
ours = { workflow, files: pack.files, version_id: versionIdFor(id, pack.digest) };
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return { status: 'skipped' };
|
|
117
|
+
}
|
|
118
|
+
const savedBy = theirs.saved_by ?? null;
|
|
119
|
+
if (ours.version_id === theirs.version_id) {
|
|
120
|
+
if (record?.version_id !== theirs.version_id)
|
|
121
|
+
recordCheckout(client.server, id, theirs.version_id, theirs.version_number, theirs.workflow, { saved_by: savedBy });
|
|
122
|
+
return { status: 'current' };
|
|
123
|
+
}
|
|
124
|
+
if (record?.version_id === theirs.version_id)
|
|
125
|
+
return { status: 'current' };
|
|
126
|
+
let base = { workflow: {}, files: [] };
|
|
127
|
+
// An older server sends no merge_base: then the version this computer last saved or got is the base.
|
|
128
|
+
const baseId = theirs.merge_base !== undefined ? theirs.merge_base : record?.version_id;
|
|
129
|
+
// This computer's version was kept beside the newest one (an earlier CLI recorded it as its last save): it is combined now.
|
|
130
|
+
const beside = record?.version_id && baseId !== record.version_id ? record.version_id : undefined;
|
|
131
|
+
if (record?.version_id && baseId) {
|
|
108
132
|
try {
|
|
109
|
-
await
|
|
110
|
-
|
|
111
|
-
// A local file that the last save did not write, and that differs, is the user's: then nothing changes.
|
|
112
|
-
for (const f of incoming) {
|
|
113
|
-
const local = join(root, f.path);
|
|
114
|
-
if (existsSync(local) && !home.files?.[f.path] && fileHash(readFileSync(local)) !== f.sha256)
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
for (const f of incoming) {
|
|
118
|
-
mkdirSync(dirname(join(root, f.path)), { recursive: true, mode: 0o700 });
|
|
119
|
-
copyFileSync(join(staging, f.path), join(root, f.path));
|
|
120
|
-
}
|
|
133
|
+
const saved = await client.request(`${methodPath}?version=${encodeURIComponent(baseId)}`, 'GET', undefined, true, options);
|
|
134
|
+
base = { workflow: saved.workflow, files: saved.package?.files ?? [] };
|
|
121
135
|
}
|
|
122
|
-
|
|
123
|
-
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (unreachable(error))
|
|
138
|
+
return { status: 'offline', line: `Could not check for a newer version; ${doing} this file.` };
|
|
139
|
+
base = { workflow: record.workflow ?? {}, files: [] };
|
|
124
140
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
141
|
+
}
|
|
142
|
+
const { mergeMethod, mergeFiles, changedSteps } = await import('./merge.js');
|
|
143
|
+
const theirFiles = theirs.package?.files ?? [];
|
|
144
|
+
const steps = mergeMethod(base.workflow, ours.workflow, theirs.workflow), files = mergeFiles(base.files, ours.files, theirFiles);
|
|
145
|
+
const ourHashes = new Map(ours.files.map(f => [f.path, f.sha256]));
|
|
146
|
+
// Files that come from theirs. A file here that no save wrote and that differs is the person's own: a conflict too.
|
|
147
|
+
const incoming = files.files.filter(f => ourHashes.get(f.path) !== f.sha256);
|
|
148
|
+
const local = (name) => { try {
|
|
149
|
+
return fileHash(readFileSync(join(root, name)));
|
|
133
150
|
}
|
|
134
151
|
catch {
|
|
135
|
-
return
|
|
152
|
+
return undefined;
|
|
153
|
+
} };
|
|
154
|
+
const { isDeepStrictEqual } = await import('node:util');
|
|
155
|
+
// After a combine stopped, a part whose text changed on this computer since then is combined by the person: theirs is used.
|
|
156
|
+
const stop = record?.conflict?.theirs === theirs.version_id ? record.conflict : undefined;
|
|
157
|
+
const combinedStep = (at, now) => !!stop && at in stop.parts && !isDeepStrictEqual(now, stop.parts[at]);
|
|
158
|
+
const combinedFile = (at) => !!stop && at in stop.files && (local(at) ?? null) !== stop.files[at];
|
|
159
|
+
const fileConflicts = [...files.conflicts.map(c => c.path), ...incoming.filter(f => !ourHashes.has(f.path) && local(f.path) !== undefined && local(f.path) !== f.sha256).map(f => f.path)];
|
|
160
|
+
const stepConflicts = steps.clean ? [] : steps.conflicts;
|
|
161
|
+
const openSteps = stepConflicts.filter(c => !combinedStep(c.path, c.ours)), openFiles = fileConflicts.filter(at => !combinedFile(at));
|
|
162
|
+
const keepLocal = new Set(fileConflicts.filter(combinedFile));
|
|
163
|
+
const take = incoming.filter(f => !keepLocal.has(f.path));
|
|
164
|
+
const download = async (f) => {
|
|
165
|
+
const bytes = await client.transfer(`${methodPath}/files/${f.sha256}?version=${encodeURIComponent(theirs.version_id)}`);
|
|
166
|
+
if (fileHash(bytes) !== f.sha256)
|
|
167
|
+
throw Error(`The newer version's file ${f.path} does not match its checksum.`);
|
|
168
|
+
return bytes;
|
|
169
|
+
};
|
|
170
|
+
const who = savedBy ?? 'A teammate', name = relative(root, path);
|
|
171
|
+
if (openSteps.length || openFiles.length) {
|
|
172
|
+
// Remember this computer's text of each part, so that the next run sees which parts the person combined.
|
|
173
|
+
const { recordCombineStop } = await import('./outbox.js');
|
|
174
|
+
recordCombineStop(client.server, id, {
|
|
175
|
+
theirs: theirs.version_id,
|
|
176
|
+
parts: Object.fromEntries(stepConflicts.map(c => [c.path, combinedStep(c.path, c.ours) ? stop.parts[c.path] : c.ours])),
|
|
177
|
+
files: Object.fromEntries(fileConflicts.map(at => [at, combinedFile(at) ? stop.files[at] : local(at) ?? null])),
|
|
178
|
+
});
|
|
179
|
+
const conflicts = [];
|
|
180
|
+
for (const c of openSteps)
|
|
181
|
+
conflicts.push({ part: c.path.replace(/^steps\./, 'step '), file: name, yours: c.ours, theirs: c.theirs });
|
|
182
|
+
for (const at of openFiles) {
|
|
183
|
+
const theirFile = theirFiles.find(f => f.path === at);
|
|
184
|
+
const text = async () => { try {
|
|
185
|
+
return clip(Buffer.from(await download(theirFile)).toString('utf8'));
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
return `(not read: ${error.message})`;
|
|
189
|
+
} };
|
|
190
|
+
conflicts.push({ part: `file ${at}`, file: at, yours: (() => { try {
|
|
191
|
+
return clip(readFileSync(join(root, at), 'utf8'));
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return null;
|
|
195
|
+
} })(), theirs: theirFile ? await text() : null });
|
|
196
|
+
}
|
|
197
|
+
const where = [...new Set(conflicts.map(c => c.file))];
|
|
198
|
+
const headline = `${who} also changed ${conflicts.map(c => c.part).join(', ')}. Combine the two texts in ${where.join(' and ')}, then run again.`;
|
|
199
|
+
return { status: 'conflict', result: { status: 'needs_you', failure: combineFailure(headline, conflicts, who), conflicts } };
|
|
136
200
|
}
|
|
201
|
+
try {
|
|
202
|
+
loadWorkflow(steps.workflow);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
const headline = `Your changes and ${savedBy ? `${savedBy}'s` : 'the newest'} version combine into a Method that does not load: ${error.message} Combine them in ${name} by hand, then run again.`;
|
|
206
|
+
return { status: 'conflict', result: { status: 'needs_you', failure: combineFailure(headline, [], who), conflicts: [] } };
|
|
207
|
+
}
|
|
208
|
+
// Every file is read before any file changes, so a failed download changes nothing.
|
|
209
|
+
let written;
|
|
210
|
+
try {
|
|
211
|
+
written = await Promise.all(take.map(async (f) => ({ path: f.path, bytes: await download(f) })));
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
return unreachable(error) ? { status: 'offline', line: `Could not check for a newer version; ${doing} this file.` } : { status: 'skipped' };
|
|
215
|
+
}
|
|
216
|
+
const { mkdirSync, renameSync, writeFileSync } = await import('node:fs');
|
|
217
|
+
const { packagePath } = await import('../../contracts/src/method-package.js');
|
|
218
|
+
for (const f of written) {
|
|
219
|
+
const target = resolve(root, packagePath(f.path));
|
|
220
|
+
mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
|
|
221
|
+
writeFileSync(`${target}.download`, f.bytes, { mode: 0o600 });
|
|
222
|
+
renameSync(`${target}.download`, target);
|
|
223
|
+
}
|
|
224
|
+
if (!isDeepStrictEqual(steps.workflow, ours.workflow))
|
|
225
|
+
(await import('./proposals.js')).writeMethodFile(path, id, steps.workflow);
|
|
226
|
+
(await import('./legacy.js')).convertLegacy(path);
|
|
227
|
+
const fileChanges = (from, to) => [...new Set([...from, ...to].map(f => f.path))].filter(at => from.find(f => f.path === at)?.sha256 !== to.find(f => f.path === at)?.sha256);
|
|
228
|
+
const yours = parts(changedSteps(base.workflow, ours.workflow), fileChanges(base.files, ours.files));
|
|
229
|
+
const kept = !isDeepStrictEqual(ours.workflow, base.workflow) || fileChanges(base.files, ours.files).length > 0;
|
|
230
|
+
// theirs is the parent of the next save; a side version of this computer that the file now combines goes with it.
|
|
231
|
+
recordCheckout(client.server, id, theirs.version_id, theirs.version_number, theirs.workflow, { saved_by: savedBy, ...(kept && (record?.side_version_id ?? beside) ? { merged_version_id: (record?.side_version_id ?? beside) } : {}) });
|
|
232
|
+
const changed = parts(changedSteps(base.workflow, theirs.workflow), fileChanges(base.files, theirFiles));
|
|
233
|
+
return { status: 'updated', line: `Updated to ${savedBy ? `${savedBy}'s` : 'the newest'} version${clock(theirs.saved_at)}${changed ? ` (changed ${changed})` : ''}.${kept ? ` Your changes${yours ? ` to ${yours}` : ''} are kept.` : ''}` };
|
|
234
|
+
}
|
|
235
|
+
/** The failure record of a combine that needs the person: needs_you, with both texts of each part as evidence. */
|
|
236
|
+
function combineFailure(headline, conflicts, who) {
|
|
237
|
+
const text = (value) => typeof value === 'string' ? value : JSON.stringify(value);
|
|
238
|
+
return { owner: 'needs_you', code: 'combine_versions', headline, step: null, step_name: null, iteration: null, failed_iterations: [], total: null, phase: 'setup', check_name: null,
|
|
239
|
+
next: { action: 'fix_method', text: 'Edit the file so that each part keeps both changes, then run again. No run started and no file changed.' },
|
|
240
|
+
evidence: conflicts.flatMap(c => [`Your ${c.part}: ${text(c.yours)}`, `${who}'s ${c.part}: ${text(c.theirs)}`]) };
|
|
137
241
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MethodClient } from './method-client.js';
|
|
2
|
+
import type { CorrectionOutcome } from './fix-lines.js';
|
|
2
3
|
/** A judgment as improve.method reads it. new marks the corrections to fix now; run is the account record of a run with no folder here. */
|
|
3
4
|
export type ImproveJudgment = {
|
|
4
5
|
id: string;
|
|
@@ -13,7 +14,12 @@ export type ImproveJudgment = {
|
|
|
13
14
|
created_at: string;
|
|
14
15
|
new?: boolean;
|
|
15
16
|
run?: unknown;
|
|
17
|
+
noted_by?: string | null;
|
|
18
|
+
/** The question that an earlier fix asked about this correction, and the person's answer. */
|
|
19
|
+
question?: string;
|
|
20
|
+
answer?: string;
|
|
16
21
|
};
|
|
22
|
+
export type { CorrectionOutcome };
|
|
17
23
|
/** Runs a local Method file; the default is the runtime that `method run FILE` uses. */
|
|
18
24
|
export type RunFile = (file: string, flags: Record<string, unknown>, syncFactory: undefined, onEvent: undefined, client: MethodClient, baseConfig: undefined, secrets: Record<string, string>) => Promise<{
|
|
19
25
|
status: string;
|
|
@@ -35,12 +41,17 @@ export type ImproveProposal = {
|
|
|
35
41
|
[key: string]: unknown;
|
|
36
42
|
};
|
|
37
43
|
};
|
|
44
|
+
export type ImproveResult = ImproveProposal & {
|
|
45
|
+
capped: string;
|
|
46
|
+
outcomes: CorrectionOutcome[];
|
|
47
|
+
background_cases: string[];
|
|
48
|
+
summary: string;
|
|
49
|
+
};
|
|
38
50
|
export type FixOutcome = {
|
|
39
51
|
file: string;
|
|
40
|
-
|
|
41
|
-
from_dashboard: number;
|
|
52
|
+
judgments: string[];
|
|
42
53
|
kind: 'change' | 'none';
|
|
43
|
-
|
|
54
|
+
applied: boolean;
|
|
44
55
|
cause: string;
|
|
45
56
|
steps: string[];
|
|
46
57
|
cases: {
|
|
@@ -50,11 +61,9 @@ export type FixOutcome = {
|
|
|
50
61
|
reason: string;
|
|
51
62
|
}[];
|
|
52
63
|
};
|
|
53
|
-
earlier_passed: number;
|
|
54
64
|
version?: {
|
|
55
65
|
version_id: string;
|
|
56
66
|
url: string;
|
|
57
|
-
undo: string;
|
|
58
67
|
} | undefined;
|
|
59
68
|
usage: {
|
|
60
69
|
model_calls: number;
|
|
@@ -63,15 +72,31 @@ export type FixOutcome = {
|
|
|
63
72
|
} | null;
|
|
64
73
|
capped: string;
|
|
65
74
|
log: string;
|
|
66
|
-
|
|
75
|
+
outcomes: CorrectionOutcome[];
|
|
76
|
+
/** The cases that run after an applied change, and the undo record of that change. */
|
|
77
|
+
background_cases: string[];
|
|
78
|
+
undo_id?: string | undefined;
|
|
79
|
+
/** The corrections that the applied change answered, with their notes. */
|
|
80
|
+
fixed: {
|
|
81
|
+
id: string;
|
|
82
|
+
note: string | null;
|
|
83
|
+
}[];
|
|
67
84
|
};
|
|
68
85
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
86
|
+
* One outcome of improve.method in the form the lines read: {id, outcome, detail, reason?, question?, held_with?,
|
|
87
|
+
* learned_rule?, again?, tests?: {case, base, candidate: {passes, runs}, guards: []}, tried?}. A fixed outcome's detail
|
|
88
|
+
* says what is different now; a held outcome's detail is about the loop, so the line names only the other person.
|
|
71
89
|
*/
|
|
72
|
-
export declare function
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
export declare function loopOutcome(o: any): CorrectionOutcome;
|
|
91
|
+
/**
|
|
92
|
+
* Read the result of improve.method: {proposal: <SubmitProposal as JSON text>, kind, proposal_id, outcomes: <JSON text,
|
|
93
|
+
* one outcome per new correction>, background_cases: [case ids], summary: one plain line}. A change has the tested
|
|
94
|
+
* workflow and the changed package files (a script, learned.json). A pruning sweep (mode prune) has no outcomes; its
|
|
95
|
+
* summary says what it removed, and what it left because nothing checks it.
|
|
96
|
+
*/
|
|
97
|
+
export declare function readImproveResult(result: unknown, options?: {
|
|
98
|
+
prune?: boolean;
|
|
99
|
+
}): ImproveResult;
|
|
75
100
|
/**
|
|
76
101
|
* Download the published improve.method and its files into the Method cache, once per version. For developers of
|
|
77
102
|
* improve.method: METHOD_IMPROVE_METHOD=PATH runs that local improve.method file instead, from its own folder.
|
|
@@ -82,13 +107,12 @@ export declare function cachedImproveMethod(client: MethodClient): Promise<{
|
|
|
82
107
|
version_id: string;
|
|
83
108
|
}>;
|
|
84
109
|
/**
|
|
85
|
-
* Run improve.method on this computer. Its model steps and its scripts' model requests follow the rule of every run:
|
|
110
|
+
* Run improve.method on this computer (mode prune: the pruning pass). Its model steps and its scripts' model requests follow the rule of every run:
|
|
86
111
|
* the user's own Claude Code or Codex first, hosted models only without one. The run gets the signed-in key and server
|
|
87
112
|
* as its IMPROVE_API_KEY and IMPROVE_SERVER secrets, for this run only. Its output goes to a log file.
|
|
88
113
|
*/
|
|
89
|
-
export declare function runImprovement(client: MethodClient, file: string, judgments: ImproveJudgment[], runFolders: string[], run?: RunFile): Promise<{
|
|
114
|
+
export declare function runImprovement(client: MethodClient, file: string, judgments: ImproveJudgment[], runFolders: string[], run?: RunFile, mode?: 'prune'): Promise<{
|
|
90
115
|
log: string;
|
|
91
|
-
summary: any;
|
|
92
116
|
cause: string;
|
|
93
117
|
workflow: unknown | null;
|
|
94
118
|
files: {
|
|
@@ -102,31 +126,129 @@ export declare function runImprovement(client: MethodClient, file: string, judgm
|
|
|
102
126
|
[key: string]: unknown;
|
|
103
127
|
};
|
|
104
128
|
capped: string;
|
|
129
|
+
outcomes: CorrectionOutcome[];
|
|
130
|
+
background_cases: string[];
|
|
131
|
+
summary: string;
|
|
105
132
|
}>;
|
|
106
133
|
/** The ID of the regression case that a correction makes: one per judgment, so a later loop finds it. */
|
|
107
134
|
export declare const correctionCaseId: (judgmentId: string) => string;
|
|
135
|
+
/** One thing that the person saw: the result, a step output, a file the run wrote, or a call or effect it made. */
|
|
136
|
+
type Seen = {
|
|
137
|
+
ref: string;
|
|
138
|
+
label: string;
|
|
139
|
+
value: unknown;
|
|
140
|
+
};
|
|
141
|
+
/** What the person saw of a run, with the ref that a case uses for each thing (runtime 0.16.0 refs). */
|
|
142
|
+
export declare function seenOfRun(outcome: any): Seen[];
|
|
108
143
|
/**
|
|
109
|
-
* Make the regression case of a correction, even from vague words such as "it's not even redlined"
|
|
110
|
-
*
|
|
111
|
-
*
|
|
144
|
+
* Make the regression case of a correction, even from vague words such as "it's not even redlined". A model reads what
|
|
145
|
+
* the person saw (the result, the files the run wrote, and the calls and effects it made) and writes 1 to 3 plain
|
|
146
|
+
* sentences, each about one of those things; for a note about form or a missing action, also a small check script. A
|
|
147
|
+
* sentence is kept only when it fails on the corrected run and passes on every approved run of the same part, with two
|
|
148
|
+
* judges when both Claude Code and Codex are signed in. Sentences that do not fail get one more try with the reason.
|
|
112
149
|
*/
|
|
113
150
|
export declare function caseFromCorrection(file: string, judgment: {
|
|
114
151
|
id: string;
|
|
115
152
|
note: string | null;
|
|
116
153
|
corrected?: unknown;
|
|
117
|
-
|
|
154
|
+
noted_by?: string | null;
|
|
155
|
+
answer?: string | null;
|
|
156
|
+
}, runDir: string, options?: {
|
|
157
|
+
approvedRuns?: string[];
|
|
158
|
+
}): Promise<{
|
|
118
159
|
id: string;
|
|
119
160
|
made: boolean;
|
|
120
161
|
}>;
|
|
162
|
+
/** The corrections of a Method that wait for a fix. */
|
|
163
|
+
export declare function waitingCorrections(client: MethodClient, file: string, methodId: string, waitMs?: number): Promise<{
|
|
164
|
+
note: string | null;
|
|
165
|
+
corrected: {} | null;
|
|
166
|
+
answer: string | null;
|
|
167
|
+
run_dir: string | undefined;
|
|
168
|
+
id: string;
|
|
169
|
+
run_id: string;
|
|
170
|
+
step_id: string | null;
|
|
171
|
+
verdict: "right" | "wrong";
|
|
172
|
+
source: string;
|
|
173
|
+
created_at: string;
|
|
174
|
+
pending: boolean;
|
|
175
|
+
noted_by?: string | null;
|
|
176
|
+
outcome?: string | null;
|
|
177
|
+
handled_by?: string | null;
|
|
178
|
+
held_with?: string | null;
|
|
179
|
+
question?: string | null;
|
|
180
|
+
}[]>;
|
|
121
181
|
/**
|
|
122
|
-
* Fix the pending corrections of a Method file on this computer: the loop. Returns undefined when nothing
|
|
123
|
-
* takes the pending corrections in the account first, so two computers never fix one correction twice, and
|
|
124
|
-
* back when the loop stops before it decides.
|
|
125
|
-
* error.
|
|
182
|
+
* Fix the pending corrections of a Method file on this computer: one pass of the loop. Returns undefined when nothing
|
|
183
|
+
* waits. It takes the pending corrections in the account first, so two computers never fix one correction twice, and
|
|
184
|
+
* gives them back when the loop stops before it decides. Each correction gets its own outcome. A subscription limit or
|
|
185
|
+
* another failure of the model service is a plain error.
|
|
126
186
|
*/
|
|
127
187
|
export declare function fixCorrections(client: MethodClient, file: string, options?: {
|
|
128
188
|
run?: RunFile;
|
|
129
189
|
waitMs?: number;
|
|
190
|
+
skip?: Set<string>;
|
|
130
191
|
}): Promise<FixOutcome | undefined>;
|
|
131
|
-
/**
|
|
132
|
-
export declare function
|
|
192
|
+
/** Whether a fix of the Method runs on this computer now. */
|
|
193
|
+
export declare function fixRunning(methodId: string): boolean;
|
|
194
|
+
/**
|
|
195
|
+
* Fix the waiting corrections of a Method file while holding its lock, pass after pass until none waits, so the
|
|
196
|
+
* corrections that arrive during a pass join the next. Each correction's outcome is saved for method wait. After a pass
|
|
197
|
+
* that changed the file, the other cases run (improve.method's background_cases): a failure undoes that change. Then a
|
|
198
|
+
* pruning pass runs when it is due. Returns undefined when another fix of the Method holds the lock.
|
|
199
|
+
*/
|
|
200
|
+
export declare function fixWaiting(client: MethodClient, file: string, options?: {
|
|
201
|
+
run?: RunFile;
|
|
202
|
+
waitMs?: number;
|
|
203
|
+
runCases?: RunCases;
|
|
204
|
+
}): Promise<FixOutcome[] | undefined>;
|
|
205
|
+
/** Runs cases of a Method file (quality.ts runCases); tests replace it. */
|
|
206
|
+
export type RunCases = (file: string, flags: Record<string, never>, options: {
|
|
207
|
+
ids: string[];
|
|
208
|
+
}) => Promise<{
|
|
209
|
+
cases: {
|
|
210
|
+
id: string;
|
|
211
|
+
verdict: string;
|
|
212
|
+
}[];
|
|
213
|
+
}>;
|
|
214
|
+
/**
|
|
215
|
+
* The later check of an applied change: the Method's other cases (improve.method's background_cases) run on the changed
|
|
216
|
+
* file. A failing case that belongs to a retired learned rule (its from.case) brings that rule back. Any other failure
|
|
217
|
+
* undoes the change, saves the restored file as a new version, and opens the corrections it answered again, so the next
|
|
218
|
+
* method run fixes them anew. Each of these says one line on the next method run or method wait.
|
|
219
|
+
*/
|
|
220
|
+
export declare function checkBackground(client: MethodClient, file: string, fix: Pick<FixOutcome, 'background_cases' | 'undo_id' | 'fixed' | 'cause'>, options?: {
|
|
221
|
+
runCases?: RunCases;
|
|
222
|
+
}): Promise<{
|
|
223
|
+
failed: string[];
|
|
224
|
+
restored: string[];
|
|
225
|
+
undone: boolean;
|
|
226
|
+
reopened: string[];
|
|
227
|
+
}>;
|
|
228
|
+
/**
|
|
229
|
+
* The pruning pass (improve.method with mode prune): it retires learned rules and deletes prompt text that tests show
|
|
230
|
+
* no longer change any result, and reports text that no test covers without deleting it. The change applies at once,
|
|
231
|
+
* with undo; the next method run or method wait shows the sweep's summary line.
|
|
232
|
+
*/
|
|
233
|
+
export declare function pruneMethod(client: MethodClient, file: string, options?: {
|
|
234
|
+
run?: RunFile;
|
|
235
|
+
}): Promise<{
|
|
236
|
+
changed: boolean;
|
|
237
|
+
} | undefined>;
|
|
238
|
+
/**
|
|
239
|
+
* Fix the Method for one correction: run the fix, or, when a fix of the Method already runs on this computer, wait for
|
|
240
|
+
* the fix that takes this correction. Returns the stored outcome, or undefined when no fix here took it.
|
|
241
|
+
*/
|
|
242
|
+
export declare function fixCorrection(client: MethodClient, file: string, judgmentId: string, options?: {
|
|
243
|
+
run?: RunFile;
|
|
244
|
+
pollMs?: number;
|
|
245
|
+
}): Promise<import("./fix-outcomes.js").StoredOutcome | undefined>;
|
|
246
|
+
/**
|
|
247
|
+
* `method __fix FILE --judgment ID... [--prune]`: the fix of method judge, method answer and method run, in its own
|
|
248
|
+
* process. Every correction it was started for ends with a stored outcome or error.
|
|
249
|
+
*/
|
|
250
|
+
export declare function backgroundFix(client: MethodClient, file: string, ids: string[], options?: {
|
|
251
|
+
run?: RunFile;
|
|
252
|
+
prune?: boolean;
|
|
253
|
+
pollMs?: number;
|
|
254
|
+
}): Promise<void>;
|