@haystackeditor/cli 0.22.2 → 0.23.1
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.
|
@@ -8,7 +8,7 @@ import chalk from 'chalk';
|
|
|
8
8
|
import { NotLoggedInError, resolveAuthContext } from '../utils/auth.js';
|
|
9
9
|
import { findGitRoot, parseRemoteUrl, } from '../utils/git.js';
|
|
10
10
|
import { resolveVerifyBaseSha } from '../utils/verify-base.js';
|
|
11
|
-
import { HaystackApiError, haystackJson, REPO_NOT_ENTITLED_CODE, SUBSCRIPTION_REQUIRED_CODE, } from '../utils/haystack-api.js';
|
|
11
|
+
import { HaystackApiError, haystackFetch, haystackJson, REPO_NOT_ENTITLED_CODE, SUBSCRIPTION_REQUIRED_CODE, } from '../utils/haystack-api.js';
|
|
12
12
|
import { trackPrecomputeEvent } from '../utils/telemetry.js';
|
|
13
13
|
import { findSecretsInPatch, isSecretBearingPath } from '../utils/secret-paths.js';
|
|
14
14
|
import { startPrecomputeDelivery } from './precompute-delivery.js';
|
|
@@ -196,11 +196,11 @@ function deriveChangeTitle(gitRoot, baseSha, treeSha, commits, deadline) {
|
|
|
196
196
|
}
|
|
197
197
|
function slimInputHash() {
|
|
198
198
|
// Mirrors front_half_cache_derive in blast-cache.sh for a slim bundle:
|
|
199
|
-
// printf '%s\0%s\0%s\0%s\0%s\0' slim agent-deferred none none crawl-blast-
|
|
199
|
+
// printf '%s\0%s\0%s\0%s\0%s\0' slim agent-deferred none none crawl-blast-groups-v2.
|
|
200
200
|
// The fifth field (CRAWL-V1 rule 4) keeps bundles written before
|
|
201
201
|
// crawl-blast.json existed from being restored for a crawl.
|
|
202
202
|
return createHash('sha256')
|
|
203
|
-
.update(Buffer.from('slim\0agent-deferred\0none\0none\0crawl-blast-
|
|
203
|
+
.update(Buffer.from('slim\0agent-deferred\0none\0none\0crawl-blast-groups-v2\0'))
|
|
204
204
|
.digest('hex');
|
|
205
205
|
}
|
|
206
206
|
async function buildCompressedPatch(gitRoot, baseSha, workCommit, deadline) {
|
|
@@ -313,13 +313,45 @@ function precomputeResult(response) {
|
|
|
313
313
|
return { outcome: 'existing', ...reported };
|
|
314
314
|
throw new PrecomputeFailure('The precompute service returned an invalid response.');
|
|
315
315
|
}
|
|
316
|
+
/** The captured change as the service takes it (CRAWL-V1 amendment 14): uploaded once, content-addressed by its sha256 in
|
|
317
|
+
* the repository's scope (a HEAD finds an earlier upload; a PUT only when it is missing), and named in the front-half
|
|
318
|
+
* request by digest and size instead of carried inline. Nothing on the way holds a size limit of its own: tldraw's 13.8 MB
|
|
319
|
+
* capture was refused at the 8 MiB inline bound (2026-10-02). `force` uploads even when the HEAD found it (a 409 says the
|
|
320
|
+
* service no longer has it). */
|
|
321
|
+
async function uploadSourcePatch(request, token, signal, force = false) {
|
|
322
|
+
const gzipped = Buffer.from(request.patchGzBase64, 'base64');
|
|
323
|
+
const path = `/api/agent/cloud-verifier/source-patches/${request.patchSha256}`
|
|
324
|
+
+ `?owner=${encodeURIComponent(request.owner)}&repository=${encodeURIComponent(request.repository)}`;
|
|
325
|
+
const found = force ? null : await haystackFetch(path, token, { method: 'HEAD', signal }, [404]);
|
|
326
|
+
if (found === null || found.status === 404) {
|
|
327
|
+
await haystackFetch(path, token, { method: 'PUT', headers: { 'Content-Type': 'application/gzip' }, body: gzipped, signal });
|
|
328
|
+
}
|
|
329
|
+
return gzipped.byteLength;
|
|
330
|
+
}
|
|
331
|
+
/** One front-half submission of a captured request: the change uploaded (uploadSourcePatch), then the request naming it. A
|
|
332
|
+
* 409 (the service has no such upload: it expired between the HEAD and the POST) uploads it again and submits once more. */
|
|
333
|
+
async function submitCapture(request, token, signal) {
|
|
334
|
+
const { patchGzBase64: _inline, ...named } = request;
|
|
335
|
+
const submit = async (force) => {
|
|
336
|
+
const patchBytes = await uploadSourcePatch(request, token, signal, force);
|
|
337
|
+
return haystackJson('/api/agent/cloud-verifier/front-half', token, { method: 'POST',
|
|
338
|
+
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...named, patchBytes }), signal });
|
|
339
|
+
};
|
|
340
|
+
try {
|
|
341
|
+
return await submit(false);
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
if (!(error instanceof HaystackApiError && error.status === 409))
|
|
345
|
+
throw error;
|
|
346
|
+
return submit(true);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
316
349
|
/** One authenticated submission of the already captured request. No source reads or retries. */
|
|
317
350
|
export const deliverPrecomputeRequest = async (request, signal) => {
|
|
318
351
|
signal.throwIfAborted();
|
|
319
352
|
const auth = await resolveAuthContext({ owner: request.owner, repo: request.repository });
|
|
320
353
|
signal.throwIfAborted();
|
|
321
|
-
return precomputeResult(await
|
|
322
|
-
body: JSON.stringify(request), signal }));
|
|
354
|
+
return precomputeResult(await submitCapture(request, auth.token, signal));
|
|
323
355
|
};
|
|
324
356
|
function readWalkerStatus(value) {
|
|
325
357
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
@@ -506,12 +538,7 @@ export async function postPrecomputeCapture(capture, token, deadline) {
|
|
|
506
538
|
const fetchTimer = setTimeout(() => controller.abort(), remainingMs(deadline));
|
|
507
539
|
let response;
|
|
508
540
|
try {
|
|
509
|
-
response = await
|
|
510
|
-
method: 'POST',
|
|
511
|
-
headers: { 'Content-Type': 'application/json' },
|
|
512
|
-
body: JSON.stringify(capture.derivation.request),
|
|
513
|
-
signal: controller.signal,
|
|
514
|
-
});
|
|
541
|
+
response = await submitCapture(capture.derivation.request, token, controller.signal);
|
|
515
542
|
}
|
|
516
543
|
finally {
|
|
517
544
|
clearTimeout(fetchTimer);
|
package/dist/commands/verify.js
CHANGED
|
@@ -160,6 +160,19 @@ function checkSpot(value) {
|
|
|
160
160
|
|| (reached.page !== null && typeof reached.page !== 'string')))
|
|
161
161
|
invalid('how a changed spot was reached');
|
|
162
162
|
}
|
|
163
|
+
function checkPlanningCoverage(value) {
|
|
164
|
+
if (!isRecord(value) || !['groups', 'selectedIncidentPairs', 'unplannedIncidentPairs', 'proposedSituations', 'selectedSituations',
|
|
165
|
+
'deferredSituations', 'unrealizedSituations', 'completedSituations', 'blockedSituations'].every(key => isCount(value[key]))
|
|
166
|
+
|| typeof value.sourceGraphTruncated !== 'boolean' || !Array.isArray(value.unresolvedChanges))
|
|
167
|
+
invalid('its planning coverage');
|
|
168
|
+
const changes = new Set();
|
|
169
|
+
for (const item of value.unresolvedChanges) {
|
|
170
|
+
if (!isRecord(item) || typeof item.changeKey !== 'string' || !item.changeKey || typeof item.reason !== 'string' || !item.reason
|
|
171
|
+
|| changes.has(item.changeKey))
|
|
172
|
+
invalid('its unresolved changes');
|
|
173
|
+
changes.add(item.changeKey);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
163
176
|
/** The published results' shape, shared by the sealed manifest and (amendment 9) an answer; `what` names which. */
|
|
164
177
|
function checkResults(value, view, what) {
|
|
165
178
|
if (!isRecord(value) || value.schemaVersion !== 1)
|
|
@@ -196,6 +209,8 @@ function checkResults(value, view, what) {
|
|
|
196
209
|
|| standIns.coverage.length > CRAWL_MAX_STAND_IN_ROWS || !standIns.coverage.every(isStandInRow)
|
|
197
210
|
|| !isCount(standIns.omittedRows) || !isStrings(standIns.gaps, CRAWL_MAX_STAND_IN_ROWS)))
|
|
198
211
|
invalid('its outside-service coverage');
|
|
212
|
+
if (Object.hasOwn(reach, 'planning'))
|
|
213
|
+
checkPlanningCoverage(reach.planning);
|
|
199
214
|
if (value.error !== null)
|
|
200
215
|
checkError(value.error, 'its error');
|
|
201
216
|
}
|
|
@@ -439,6 +454,17 @@ export function formatCrawl(view) {
|
|
|
439
454
|
const blast = published.reach.blast;
|
|
440
455
|
if (blast !== null)
|
|
441
456
|
lines.push(` ${blast.ran} of ${plural(blast.functions, 'affected function')} ran.`);
|
|
457
|
+
const planning = published.reach.planning;
|
|
458
|
+
if (planning !== undefined) {
|
|
459
|
+
lines.push('', chalk.bold('Situations considered'), ` Considered ${plural(planning.groups, 'group')} of related behavior, with ${plural(planning.selectedIncidentPairs, 'possible failure pattern')} selected across them.`, ` ${planning.selectedSituations} of ${plural(planning.proposedSituations, 'proposed situation')} selected; ${planning.deferredSituations} deferred.`, ` Actions completed for ${plural(planning.completedSituations, 'situation')}; ${planning.blockedSituations} blocked during execution.`, ` Could not plan checks for ${plural(planning.unplannedIncidentPairs, 'selected failure pattern')}; ${plural(planning.unrealizedSituations, 'selected situation')} had no executable route.`, ' Completed actions do not establish that a situation is free of bugs.');
|
|
460
|
+
if (planning.sourceGraphTruncated)
|
|
461
|
+
lines.push(' The recorded code relationships were incomplete.');
|
|
462
|
+
if (planning.unresolvedChanges.length) {
|
|
463
|
+
lines.push(' Changes that could not be included in a behavior group:');
|
|
464
|
+
for (const change of planning.unresolvedChanges)
|
|
465
|
+
lines.push(` ${safe(change.changeKey)}: ${safe(change.reason)}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
442
468
|
// Amendment 13: calls to outside services the stand-ins could not serve: the copy's missing coverage, never a finding.
|
|
443
469
|
const gaps = published.standIns?.gaps ?? [];
|
|
444
470
|
if (gaps.length > 0) {
|
|
@@ -183,6 +183,11 @@ export async function classifyHttpError(response, context) {
|
|
|
183
183
|
return new HaystackApiError(response.status, `${context}: unexpected response (HTTP ${response.status})${suffix}.`);
|
|
184
184
|
}
|
|
185
185
|
export async function haystackJson(path, token, init = {}, acceptedStatuses = []) {
|
|
186
|
+
const response = await haystackFetch(path, token, init, acceptedStatuses);
|
|
187
|
+
return response.json();
|
|
188
|
+
}
|
|
189
|
+
/** An authenticated request whose response is not JSON (a HEAD, an upload): the response itself, or the classified error. */
|
|
190
|
+
export async function haystackFetch(path, token, init = {}, acceptedStatuses = []) {
|
|
186
191
|
const response = await fetch(haystackApiUrl(path), {
|
|
187
192
|
...init,
|
|
188
193
|
headers: headers(token, init.headers),
|
|
@@ -190,7 +195,7 @@ export async function haystackJson(path, token, init = {}, acceptedStatuses = []
|
|
|
190
195
|
if (!response.ok && !acceptedStatuses.includes(response.status)) {
|
|
191
196
|
throw await classifyHttpError(response, `Haystack API ${path}`);
|
|
192
197
|
}
|
|
193
|
-
return response
|
|
198
|
+
return response;
|
|
194
199
|
}
|
|
195
200
|
export async function fetchCliIdentity(token) {
|
|
196
201
|
return haystackJson('/api/cli/me', token);
|