@fitlab-ai/agent-infra 0.9.5 → 0.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/dist/bin/internal-cli.js +8 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -4
- package/dist/lib/internal/codex-lifecycle.js +169 -3
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +12 -4
- package/dist/lib/internal/task-orchestration.js +40 -7
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +111 -60
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +372 -94
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/client.js +138 -22
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +142 -0
- package/dist/lib/sandbox/control/lifecycle.js +557 -0
- package/dist/lib/sandbox/control/protocol.js +69 -26
- package/dist/lib/sandbox/control/server.js +395 -99
- package/dist/lib/sandbox/control/state.js +191 -0
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +22 -19
- package/dist/lib/sandbox/recovery.js +284 -50
- package/dist/lib/sandbox/workspace-view.js +90 -10
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +17 -1
- package/dist/lib/server/process-state.js +57 -20
- package/dist/lib/task/artifact-lifecycle.js +87 -15
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +236 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +36 -9
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/delegation-receipts.js +109 -2
- package/dist/lib/task/events.js +149 -6
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +365 -15
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +78 -14
- package/dist/lib/task/verification.js +1 -0
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -3
- package/lib/internal/codex-lifecycle.ts +175 -4
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +16 -4
- package/lib/internal/task-orchestration.ts +34 -7
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +70 -25
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +423 -108
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/client.ts +152 -28
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +167 -0
- package/lib/sandbox/control/lifecycle.ts +573 -0
- package/lib/sandbox/control/protocol.ts +121 -68
- package/lib/sandbox/control/server.ts +398 -109
- package/lib/sandbox/control/state.ts +213 -0
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +16 -8
- package/lib/sandbox/recovery.ts +335 -65
- package/lib/sandbox/workspace-view.ts +102 -11
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +24 -3
- package/lib/server/process-state.ts +82 -27
- package/lib/task/artifact-lifecycle.ts +78 -14
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +319 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +52 -9
- package/lib/task/create.ts +11 -2
- package/lib/task/delegation-receipts.ts +208 -7
- package/lib/task/events.ts +131 -5
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/ledger.ts +16 -1
- package/lib/task/orchestration.ts +552 -18
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -24
- package/lib/task/verification.ts +2 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
- package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +8 -8
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
- package/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
|
@@ -9,6 +9,7 @@ import { canonicalJsonBytes, sha256 } from './store.ts';
|
|
|
9
9
|
import type { GitHubClient } from '../platform/github-client.ts';
|
|
10
10
|
import type {
|
|
11
11
|
CapturedObject,
|
|
12
|
+
EndpointQueryMode,
|
|
12
13
|
ProcessResult,
|
|
13
14
|
RestCollectionEvidence,
|
|
14
15
|
RestPageEvidence
|
|
@@ -31,6 +32,33 @@ type GitHubCapture = {
|
|
|
31
32
|
endpoints: RestCollectionEvidence[];
|
|
32
33
|
};
|
|
33
34
|
|
|
35
|
+
type GitHubBoundaryCapture = {
|
|
36
|
+
repository: string;
|
|
37
|
+
preflightDate: string;
|
|
38
|
+
watermark: string;
|
|
39
|
+
objects: CapturedObject[];
|
|
40
|
+
endpoints: RestCollectionEvidence[];
|
|
41
|
+
responseDates: string[];
|
|
42
|
+
deferred: string[];
|
|
43
|
+
unavailable: string[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type BoundaryCaptureOptions = {
|
|
47
|
+
client?: GitHubClient;
|
|
48
|
+
fromInclusive?: string | null;
|
|
49
|
+
reconciliation?: 'incremental' | 'full';
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type EndpointDescriptor = {
|
|
53
|
+
path: string;
|
|
54
|
+
queryMode: EndpointQueryMode;
|
|
55
|
+
eventTime: (item: unknown) => string | undefined;
|
|
56
|
+
identity: (item: unknown) => string;
|
|
57
|
+
parentIdentity?: string;
|
|
58
|
+
maxItems?: number;
|
|
59
|
+
includeResource?: (item: unknown) => boolean;
|
|
60
|
+
};
|
|
61
|
+
|
|
34
62
|
function endpointForPage(endpoint: string, page: number, perPage: number): string {
|
|
35
63
|
const parsed = new URL(endpoint, 'https://api.github.invalid/');
|
|
36
64
|
if (parsed.searchParams.has('page') || parsed.searchParams.has('per_page')) {
|
|
@@ -294,6 +322,38 @@ function projectGitHubItem(item: unknown): unknown {
|
|
|
294
322
|
return output;
|
|
295
323
|
}
|
|
296
324
|
|
|
325
|
+
function projectGitHubTimelineItem(item: unknown): unknown {
|
|
326
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) throw new Error('GitHub timeline item must be an object');
|
|
327
|
+
const source = item as Record<string, unknown>;
|
|
328
|
+
if (typeof source.event !== 'string' || source.event.length === 0) throw new Error('GitHub timeline event type is missing');
|
|
329
|
+
const output: Record<string, unknown> = {};
|
|
330
|
+
for (const field of [
|
|
331
|
+
'id', 'node_id', 'event', 'created_at', 'updated_at', 'submitted_at', 'commit_id', 'sha', 'state',
|
|
332
|
+
'lock_reason', 'ref', 'ref_type'
|
|
333
|
+
]) {
|
|
334
|
+
if (source[field] !== undefined) output[field] = jsonClone(source[field]);
|
|
335
|
+
}
|
|
336
|
+
for (const field of ['actor', 'user', 'assignee', 'assigner', 'reviewer', 'requested_reviewer']) {
|
|
337
|
+
const projected = projectFields(source[field], ['id', 'login', 'name']);
|
|
338
|
+
if (projected) output[field] = projected;
|
|
339
|
+
}
|
|
340
|
+
const nestedFields: Record<string, string[]> = {
|
|
341
|
+
label: ['id', 'name', 'color'],
|
|
342
|
+
milestone: ['id', 'number', 'title', 'state'],
|
|
343
|
+
rename: ['from', 'to'],
|
|
344
|
+
source: ['id', 'node_id', 'number', 'title', 'ref', 'sha'],
|
|
345
|
+
dismissed_review: ['state', 'review_id', 'dismissal_commit_id'],
|
|
346
|
+
project_card: ['id', 'project_id', 'column_name']
|
|
347
|
+
};
|
|
348
|
+
for (const [field, fields] of Object.entries(nestedFields)) {
|
|
349
|
+
const projected = projectFields(source[field], fields);
|
|
350
|
+
if (projected) output[field] = projected;
|
|
351
|
+
}
|
|
352
|
+
const body = projectSensitiveText(source.body);
|
|
353
|
+
if (body !== undefined) output.body = body;
|
|
354
|
+
return output;
|
|
355
|
+
}
|
|
356
|
+
|
|
297
357
|
function collectGitHubObjects(repoRoot: string, client: GitHubClient = createGitHubClient()): ProcessResult<GitHubCapture> {
|
|
298
358
|
const repoResult = client.json<{ nameWithOwner?: string }>(['repo', 'view', '--json', 'nameWithOwner'], { cwd: repoRoot });
|
|
299
359
|
if (!repoResult.ok) return { ok: false, error: repoResult.error };
|
|
@@ -350,5 +410,392 @@ function collectGitHubObjects(repoRoot: string, client: GitHubClient = createGit
|
|
|
350
410
|
return { ok: true, value: { repository, objects, endpoints: evidence } };
|
|
351
411
|
}
|
|
352
412
|
|
|
353
|
-
|
|
354
|
-
|
|
413
|
+
function resolveGitHubRepository(repoRoot: string, client: GitHubClient = createGitHubClient()): ProcessResult<string> {
|
|
414
|
+
const result = client.json<{ nameWithOwner?: string }>(['repo', 'view', '--json', 'nameWithOwner'], { cwd: repoRoot });
|
|
415
|
+
if (!result.ok) return { ok: false, error: result.error };
|
|
416
|
+
if (!result.value.nameWithOwner || !/^[^/]+\/[^/]+$/.test(result.value.nameWithOwner)) {
|
|
417
|
+
return { ok: false, error: { code: 'INVALID_PLATFORM_RESPONSE', message: 'GitHub repository identity is missing' } };
|
|
418
|
+
}
|
|
419
|
+
return { ok: true, value: result.value.nameWithOwner };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function truncateSecond(date: Date): string {
|
|
423
|
+
return new Date(Math.floor(date.getTime() / 1000) * 1000).toISOString();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function validDate(value: string | undefined): Date | null {
|
|
427
|
+
if (!value) return null;
|
|
428
|
+
const parsed = new Date(value);
|
|
429
|
+
return Number.isFinite(parsed.getTime()) ? parsed : null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function queryEndpoint(endpoint: string, page: number, perPage: number, queryMode: EndpointQueryMode, queryAfter: string | null): string {
|
|
433
|
+
const parsed = new URL(endpoint, 'https://api.github.invalid/');
|
|
434
|
+
parsed.searchParams.set('per_page', String(perPage));
|
|
435
|
+
parsed.searchParams.set('page', String(page));
|
|
436
|
+
if (queryMode === 'strict-since' && queryAfter) parsed.searchParams.set('since', queryAfter);
|
|
437
|
+
return `${parsed.pathname.replace(/^\//, '')}${parsed.search}`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function resourceIdentity(prefix: string, item: unknown): string {
|
|
441
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return '';
|
|
442
|
+
const value = item as Record<string, unknown>;
|
|
443
|
+
if (typeof value.id === 'number' || typeof value.id === 'string') return `${prefix}:${String(value.id)}`;
|
|
444
|
+
if (typeof value.node_id === 'string') return `${prefix}:${value.node_id}`;
|
|
445
|
+
if (typeof value.sha === 'string') return `${prefix}:${value.sha}`;
|
|
446
|
+
if (typeof value.number === 'number') return `${prefix}:${value.number}`;
|
|
447
|
+
return '';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function issueOrPullIdentity(item: unknown): string {
|
|
451
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return '';
|
|
452
|
+
const value = item as Record<string, unknown>;
|
|
453
|
+
const prefix = value.pull_request ? 'pr' : 'issue';
|
|
454
|
+
return resourceIdentity(prefix, item);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function isPullRequestIssue(item: unknown): boolean {
|
|
458
|
+
return Boolean(item && typeof item === 'object' && !Array.isArray(item) && (item as Record<string, unknown>).pull_request);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function itemEventTime(item: unknown): string | undefined {
|
|
462
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return undefined;
|
|
463
|
+
const value = item as Record<string, unknown>;
|
|
464
|
+
for (const key of ['updated_at', 'created_at', 'submitted_at']) {
|
|
465
|
+
if (typeof value[key] === 'string') return value[key];
|
|
466
|
+
}
|
|
467
|
+
const commit = value.commit as Record<string, unknown> | undefined;
|
|
468
|
+
const author = commit?.author as Record<string, unknown> | undefined;
|
|
469
|
+
const committer = commit?.committer as Record<string, unknown> | undefined;
|
|
470
|
+
if (typeof author?.date === 'string') return author.date;
|
|
471
|
+
if (typeof committer?.date === 'string') return committer.date;
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function timelineIdentity(item: unknown, parentNumber?: string): string {
|
|
476
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return '';
|
|
477
|
+
const value = item as Record<string, unknown>;
|
|
478
|
+
if (typeof value.id === 'number' || typeof value.id === 'string') return `timeline:${String(value.id)}`;
|
|
479
|
+
const actor = value.actor as Record<string, unknown> | undefined;
|
|
480
|
+
const target = value.issue as Record<string, unknown> | undefined;
|
|
481
|
+
const tuple = [parentNumber, value.event, value.created_at ?? value.updated_at, actor?.id ?? actor?.login, target?.id ?? value.commit_id];
|
|
482
|
+
return tuple.every((part) => part !== undefined && part !== null && String(part) !== '') ? `timeline:${tuple.join(':')}` : '';
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function descriptor(
|
|
486
|
+
pathname: string,
|
|
487
|
+
queryMode: EndpointQueryMode,
|
|
488
|
+
prefix: string,
|
|
489
|
+
identity?: (item: unknown) => string,
|
|
490
|
+
options: Pick<EndpointDescriptor, 'parentIdentity' | 'maxItems' | 'includeResource'> = {}
|
|
491
|
+
): EndpointDescriptor {
|
|
492
|
+
return {
|
|
493
|
+
path: pathname,
|
|
494
|
+
queryMode,
|
|
495
|
+
eventTime: itemEventTime,
|
|
496
|
+
identity: identity ?? ((item) => resourceIdentity(prefix, item)),
|
|
497
|
+
...options
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function responseDateOrError(value: string | undefined): ProcessResult<{ date: Date; iso: string }> {
|
|
502
|
+
const date = validDate(value);
|
|
503
|
+
if (!date) return { ok: false, error: { code: 'OBSERVATION_BOUNDARY_INVALID', message: 'GitHub response Date is missing or invalid' } };
|
|
504
|
+
return { ok: true, value: { date, iso: date.toISOString() } };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function collectBoundaryCollection(
|
|
508
|
+
client: GitHubClient,
|
|
509
|
+
endpoint: EndpointDescriptor,
|
|
510
|
+
preflight: Date,
|
|
511
|
+
fromInclusive: Date | null,
|
|
512
|
+
watermark: Date,
|
|
513
|
+
reconciliation: 'incremental' | 'full',
|
|
514
|
+
addResource: (object: CapturedObject, identity: string, eventTime: string) => ProcessResult<void>,
|
|
515
|
+
deferred: Set<string>,
|
|
516
|
+
responseDates: string[]
|
|
517
|
+
): ProcessResult<RestCollectionEvidence> {
|
|
518
|
+
const perPage = 100;
|
|
519
|
+
const queryMode = reconciliation === 'full' ? 'full-enumeration' : (fromInclusive ? endpoint.queryMode : 'full-enumeration');
|
|
520
|
+
const queryAfter = queryMode === 'strict-since' && fromInclusive
|
|
521
|
+
? new Date(fromInclusive.getTime() - 1000).toISOString()
|
|
522
|
+
: null;
|
|
523
|
+
const pages: RestPageEvidence[] = [];
|
|
524
|
+
const objects: CapturedObject[] = [];
|
|
525
|
+
const identities = new Set<string>();
|
|
526
|
+
let previousHash: string | null = null;
|
|
527
|
+
|
|
528
|
+
for (let page = 1; ; page += 1) {
|
|
529
|
+
const url = queryEndpoint(endpoint.path, page, perPage, queryMode, queryAfter);
|
|
530
|
+
if (!client.jsonWithMetadata) {
|
|
531
|
+
return { ok: false, error: { code: 'PLATFORM_METADATA_UNAVAILABLE', message: 'GitHub response metadata boundary is unavailable' } };
|
|
532
|
+
}
|
|
533
|
+
const response = client.jsonWithMetadata<unknown>(['api', url]);
|
|
534
|
+
if (!response.ok) return { ok: false, error: response.error };
|
|
535
|
+
const dateResult = responseDateOrError(response.value.metadata.date);
|
|
536
|
+
if (!dateResult.ok) return dateResult;
|
|
537
|
+
if (dateResult.value.date.getTime() < preflight.getTime()) {
|
|
538
|
+
return { ok: false, error: { code: 'OBSERVATION_BOUNDARY_INVALID', message: `Response Date precedes preflight for ${endpoint.path}` } };
|
|
539
|
+
}
|
|
540
|
+
responseDates.push(dateResult.value.iso);
|
|
541
|
+
const actualUrl = response.value.metadata.requestUrl;
|
|
542
|
+
if (queryAfter && (!actualUrl.includes(`since=${encodeURIComponent(queryAfter)}`) && !actualUrl.includes(`since=${queryAfter}`))) {
|
|
543
|
+
return { ok: false, error: { code: 'QUERY_BOUNDARY_INVALID', message: `GitHub request did not include since=${queryAfter}` } };
|
|
544
|
+
}
|
|
545
|
+
if (!Array.isArray(response.value.value) || response.value.value.length > perPage) {
|
|
546
|
+
return { ok: false, error: { code: 'INVALID_PLATFORM_RESPONSE', message: `GitHub page ${page} is not a valid array page` } };
|
|
547
|
+
}
|
|
548
|
+
const selected = (response.value.value as unknown[]).map((item) => endpoint.path.includes('/timeline')
|
|
549
|
+
? projectGitHubTimelineItem(item)
|
|
550
|
+
: projectGitHubItem(item));
|
|
551
|
+
const bytes = canonicalJsonBytes(selected);
|
|
552
|
+
const pageHash = sha256(bytes);
|
|
553
|
+
if (previousHash === pageHash && selected.length > 0) {
|
|
554
|
+
return { ok: false, error: { code: 'PAGINATION_UNSTABLE', message: `GitHub page ${page} repeats the previous page` } };
|
|
555
|
+
}
|
|
556
|
+
let overlapItemCount = 0;
|
|
557
|
+
let acceptedItemCount = 0;
|
|
558
|
+
let deferredItemCount = 0;
|
|
559
|
+
for (let index = 0; index < response.value.value.length; index += 1) {
|
|
560
|
+
const raw = response.value.value[index];
|
|
561
|
+
const projected = selected[index];
|
|
562
|
+
const identity = endpoint.identity(raw);
|
|
563
|
+
const eventTime = endpoint.eventTime(raw);
|
|
564
|
+
if (!identity) return { ok: false, error: { code: endpoint.path.includes('/timeline') ? 'TIMELINE_IDENTITY_UNAVAILABLE' : 'RESOURCE_IDENTITY_UNAVAILABLE', message: `Stable identity is missing for ${endpoint.path}` } };
|
|
565
|
+
if (!eventTime || !validDate(eventTime)) return { ok: false, error: { code: 'RESOURCE_TIME_UNAVAILABLE', message: `Event time is missing for ${identity}` } };
|
|
566
|
+
if (identities.has(identity)) return { ok: false, error: { code: 'PAGINATION_UNSTABLE', message: `duplicate identity on ${endpoint.path}` } };
|
|
567
|
+
identities.add(identity);
|
|
568
|
+
const observed = validDate(eventTime)!;
|
|
569
|
+
if (fromInclusive && observed.getTime() < fromInclusive.getTime()) {
|
|
570
|
+
overlapItemCount += 1;
|
|
571
|
+
} else if (observed.getTime() >= watermark.getTime()) {
|
|
572
|
+
deferredItemCount += 1;
|
|
573
|
+
if (endpoint.includeResource?.(raw) !== false) deferred.add(identity);
|
|
574
|
+
} else {
|
|
575
|
+
acceptedItemCount += 1;
|
|
576
|
+
if (endpoint.includeResource?.(raw) === false) continue;
|
|
577
|
+
const resourceBytes = canonicalJsonBytes(projected);
|
|
578
|
+
const rawRecord = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
|
|
579
|
+
const routeNumber = typeof rawRecord.number === 'number' ? rawRecord.number : undefined;
|
|
580
|
+
const added = addResource({
|
|
581
|
+
sourceKind: 'github-rest',
|
|
582
|
+
sourceIdentity: identity,
|
|
583
|
+
resourceIdentity: identity,
|
|
584
|
+
sha256: sha256(resourceBytes),
|
|
585
|
+
bytes: resourceBytes.length,
|
|
586
|
+
content: resourceBytes.toString('utf8'),
|
|
587
|
+
disposition: { state: 'included' },
|
|
588
|
+
role: 'resource',
|
|
589
|
+
eventTime,
|
|
590
|
+
...(routeNumber !== undefined ? { routeNumber } : {}),
|
|
591
|
+
...(endpoint.parentIdentity ? { parentIdentity: endpoint.parentIdentity } : {}),
|
|
592
|
+
pageSha256: pageHash
|
|
593
|
+
}, identity, eventTime);
|
|
594
|
+
if (!added.ok) return added;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const pageEvidence: RestPageEvidence = {
|
|
598
|
+
index: page,
|
|
599
|
+
itemCount: response.value.value.length,
|
|
600
|
+
canonicalSha256: pageHash,
|
|
601
|
+
queryMode,
|
|
602
|
+
...(queryAfter ? { requestedSince: queryAfter } : {}),
|
|
603
|
+
responseDate: dateResult.value.iso,
|
|
604
|
+
overlapItemCount,
|
|
605
|
+
acceptedItemCount,
|
|
606
|
+
deferredItemCount
|
|
607
|
+
};
|
|
608
|
+
pages.push(pageEvidence);
|
|
609
|
+
objects.push({
|
|
610
|
+
sourceKind: 'github-rest',
|
|
611
|
+
sourceIdentity: `${endpoint.path}#page=${page}`,
|
|
612
|
+
sha256: pageHash,
|
|
613
|
+
bytes: bytes.length,
|
|
614
|
+
content: bytes.toString('utf8'),
|
|
615
|
+
disposition: { state: 'included' },
|
|
616
|
+
role: 'page-evidence',
|
|
617
|
+
endpoint: endpoint.path,
|
|
618
|
+
page,
|
|
619
|
+
queryMode,
|
|
620
|
+
...(queryAfter ? { requestedSince: queryAfter } : {}),
|
|
621
|
+
responseDate: dateResult.value.iso
|
|
622
|
+
});
|
|
623
|
+
previousHash = pageHash;
|
|
624
|
+
const links = response.value.metadata.links.join(' ');
|
|
625
|
+
if (response.value.value.length < perPage) {
|
|
626
|
+
if (/rel=["']next["']/.test(links)) return { ok: false, error: { code: 'PAGINATION_UNSTABLE', message: `short page ${page} has a next link` } };
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const accepted = pages.reduce((sum, page) => sum + (page.acceptedItemCount ?? 0), 0);
|
|
631
|
+
const observed = pages.reduce((sum, page) => sum + page.itemCount, 0);
|
|
632
|
+
if (endpoint.maxItems !== undefined && observed >= endpoint.maxItems) {
|
|
633
|
+
return { ok: false, error: { code: 'PLATFORM_LIMIT_REACHED', message: `GitHub endpoint ${endpoint.path} reached its documented ${endpoint.maxItems}-item limit` } };
|
|
634
|
+
}
|
|
635
|
+
const evidence: RestCollectionEvidence = {
|
|
636
|
+
endpoint: endpoint.path,
|
|
637
|
+
requestCount: pages.length,
|
|
638
|
+
dataPageCount: pages.filter((page) => page.itemCount > 0).length,
|
|
639
|
+
itemCount: accepted,
|
|
640
|
+
termination: 'short-page',
|
|
641
|
+
pages,
|
|
642
|
+
queryMode,
|
|
643
|
+
...(queryAfter ? { requestedSince: queryAfter } : {})
|
|
644
|
+
};
|
|
645
|
+
// The caller adds these after all resources are known so page evidence stays separate from resource hashes.
|
|
646
|
+
for (const object of objects) {
|
|
647
|
+
const added = addResource(object, object.sourceIdentity, object.eventTime ?? watermark.toISOString());
|
|
648
|
+
if (!added.ok) return added;
|
|
649
|
+
}
|
|
650
|
+
return { ok: true, value: evidence };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function collectGitHubBoundary(repoRoot: string, options: BoundaryCaptureOptions = {}): ProcessResult<GitHubBoundaryCapture> {
|
|
654
|
+
const client = options.client ?? createGitHubClient();
|
|
655
|
+
const repoResult = client.json<{ nameWithOwner?: string }>(['repo', 'view', '--json', 'nameWithOwner'], { cwd: repoRoot });
|
|
656
|
+
if (!repoResult.ok) return { ok: false, error: repoResult.error };
|
|
657
|
+
const repository = repoResult.value.nameWithOwner;
|
|
658
|
+
if (!repository || !/^[^/]+\/[^/]+$/.test(repository) || !client.jsonWithMetadata) {
|
|
659
|
+
return { ok: false, error: { code: 'INVALID_PLATFORM_RESPONSE', message: 'GitHub repository identity or response metadata is missing' } };
|
|
660
|
+
}
|
|
661
|
+
const preflightResult = client.jsonWithMetadata<unknown>(['api', `repos/${repository}`]);
|
|
662
|
+
if (!preflightResult.ok) return { ok: false, error: preflightResult.error };
|
|
663
|
+
const preflightDate = responseDateOrError(preflightResult.value.metadata.date);
|
|
664
|
+
if (!preflightDate.ok) return preflightDate;
|
|
665
|
+
const watermark = new Date(truncateSecond(preflightDate.value.date));
|
|
666
|
+
const fromInclusive = options.fromInclusive ? validDate(options.fromInclusive) : null;
|
|
667
|
+
if (options.fromInclusive && !fromInclusive) return { ok: false, error: { code: 'CHECKPOINT_INVALID', message: 'Checkpoint watermark is invalid' } };
|
|
668
|
+
if (fromInclusive && watermark.getTime() <= fromInclusive.getTime()) {
|
|
669
|
+
return { ok: false, error: { code: 'OBSERVATION_BOUNDARY_INVALID', message: 'Observation cutoff is not after checkpoint watermark' } };
|
|
670
|
+
}
|
|
671
|
+
const reconciliation = options.reconciliation ?? 'incremental';
|
|
672
|
+
const allObjects: CapturedObject[] = [];
|
|
673
|
+
const resources = new Map<string, CapturedObject>();
|
|
674
|
+
const pageObjects: CapturedObject[] = [];
|
|
675
|
+
const endpoints: RestCollectionEvidence[] = [];
|
|
676
|
+
const deferred = new Set<string>();
|
|
677
|
+
const unavailable = new Set<string>();
|
|
678
|
+
const responseDates = [preflightDate.value.iso];
|
|
679
|
+
const addResource = (object: CapturedObject, identity: string): ProcessResult<void> => {
|
|
680
|
+
if (object.role === 'page-evidence') {
|
|
681
|
+
pageObjects.push(object);
|
|
682
|
+
return { ok: true, value: undefined };
|
|
683
|
+
}
|
|
684
|
+
if (identity) {
|
|
685
|
+
const existing = resources.get(identity);
|
|
686
|
+
if (existing && (existing.sha256 !== object.sha256 || existing.parentIdentity !== object.parentIdentity)) {
|
|
687
|
+
return { ok: false, error: { code: 'RESOURCE_IDENTITY_COLLISION', message: `Conflicting GitHub resources share identity ${identity}` } };
|
|
688
|
+
}
|
|
689
|
+
if (!existing) resources.set(identity, object);
|
|
690
|
+
}
|
|
691
|
+
return { ok: true, value: undefined };
|
|
692
|
+
};
|
|
693
|
+
const collect = (endpoint: EndpointDescriptor): ProcessResult<void> => {
|
|
694
|
+
const result = collectBoundaryCollection(client, endpoint, preflightDate.value.date, fromInclusive, watermark, reconciliation, addResource, deferred, responseDates);
|
|
695
|
+
if (!result.ok) return result;
|
|
696
|
+
endpoints.push(result.value);
|
|
697
|
+
return { ok: true, value: undefined };
|
|
698
|
+
};
|
|
699
|
+
const issueEndpoint = `repos/${repository}/issues?state=all&sort=created&direction=asc`;
|
|
700
|
+
const pullEndpoint = `repos/${repository}/pulls?state=all&sort=created&direction=asc`;
|
|
701
|
+
for (const endpoint of [
|
|
702
|
+
descriptor(issueEndpoint, 'strict-since', 'issue', issueOrPullIdentity, { includeResource: (item) => !isPullRequestIssue(item) }),
|
|
703
|
+
descriptor(pullEndpoint, 'full-enumeration', 'pr')
|
|
704
|
+
]) {
|
|
705
|
+
const result = collect(endpoint);
|
|
706
|
+
if (!result.ok) return result;
|
|
707
|
+
}
|
|
708
|
+
const issueNumbers = new Set<string>();
|
|
709
|
+
const pullNumbers = new Set<string>();
|
|
710
|
+
const issueRoots = new Map<string, CapturedObject>();
|
|
711
|
+
const pullRoots = new Map<string, CapturedObject>();
|
|
712
|
+
for (const resource of resources.values()) {
|
|
713
|
+
if (resource.routeNumber === undefined || !resource.resourceIdentity) continue;
|
|
714
|
+
if (resource.resourceIdentity.startsWith('pr:')) {
|
|
715
|
+
const number = String(resource.routeNumber);
|
|
716
|
+
pullNumbers.add(number);
|
|
717
|
+
pullRoots.set(number, resource);
|
|
718
|
+
} else if (resource.resourceIdentity.startsWith('issue:')) {
|
|
719
|
+
const number = String(resource.routeNumber);
|
|
720
|
+
issueNumbers.add(number);
|
|
721
|
+
issueRoots.set(number, resource);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
for (const number of issueNumbers) {
|
|
725
|
+
const parentIdentity = issueRoots.get(number)?.resourceIdentity;
|
|
726
|
+
const result = collect(descriptor(`repos/${repository}/issues/${number}/comments`, 'strict-since', 'comment', undefined, { parentIdentity }));
|
|
727
|
+
if (!result.ok) return result;
|
|
728
|
+
const timeline = collect(descriptor(
|
|
729
|
+
`repos/${repository}/issues/${number}/timeline`,
|
|
730
|
+
'full-enumeration',
|
|
731
|
+
'timeline',
|
|
732
|
+
(item) => timelineIdentity(item, number),
|
|
733
|
+
{ parentIdentity }
|
|
734
|
+
));
|
|
735
|
+
if (!timeline.ok) return timeline;
|
|
736
|
+
}
|
|
737
|
+
for (const number of pullNumbers) {
|
|
738
|
+
const expected = pullRoots.get(number);
|
|
739
|
+
let expectedHead: string | undefined;
|
|
740
|
+
if (expected?.content) {
|
|
741
|
+
try {
|
|
742
|
+
expectedHead = ((JSON.parse(expected.content) as Record<string, unknown>).head as Record<string, unknown> | undefined)?.sha as string | undefined;
|
|
743
|
+
} catch {
|
|
744
|
+
expectedHead = undefined;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
const current = client.jsonWithMetadata<unknown>(['api', `repos/${repository}/pulls/${number}`]);
|
|
748
|
+
if (!current.ok) return { ok: false, error: current.error };
|
|
749
|
+
const currentDate = responseDateOrError(current.value.metadata.date);
|
|
750
|
+
if (!currentDate.ok) return currentDate;
|
|
751
|
+
if (currentDate.value.date.getTime() < preflightDate.value.date.getTime()) {
|
|
752
|
+
return { ok: false, error: { code: 'OBSERVATION_BOUNDARY_INVALID', message: `Response Date precedes preflight for pull request ${number}` } };
|
|
753
|
+
}
|
|
754
|
+
responseDates.push(currentDate.value.iso);
|
|
755
|
+
const currentProjected = projectGitHubItem(current.value.value) as Record<string, unknown>;
|
|
756
|
+
const currentHead = (currentProjected.head as Record<string, unknown> | undefined)?.sha;
|
|
757
|
+
if (expectedHead && currentHead !== expectedHead) {
|
|
758
|
+
return { ok: false, error: { code: 'SOURCE_DRIFT', message: `Pull request ${number} head changed during capture` } };
|
|
759
|
+
}
|
|
760
|
+
const parentIdentity = expected?.resourceIdentity;
|
|
761
|
+
const conversationComments = collect(descriptor(`repos/${repository}/issues/${number}/comments`, 'strict-since', 'comment', undefined, { parentIdentity }));
|
|
762
|
+
if (!conversationComments.ok) return conversationComments;
|
|
763
|
+
for (const suffix of ['comments', 'reviews', 'commits']) {
|
|
764
|
+
const mode: EndpointQueryMode = suffix === 'reviews' || suffix === 'commits' ? 'full-enumeration' : 'strict-since';
|
|
765
|
+
const prefix = suffix === 'comments' ? 'review-comment' : suffix === 'reviews' ? 'review' : 'commit';
|
|
766
|
+
const result = collect(descriptor(
|
|
767
|
+
`repos/${repository}/pulls/${number}/${suffix}`,
|
|
768
|
+
mode,
|
|
769
|
+
prefix,
|
|
770
|
+
undefined,
|
|
771
|
+
{ parentIdentity, ...(suffix === 'commits' ? { maxItems: 250 } : {}) }
|
|
772
|
+
));
|
|
773
|
+
if (!result.ok) return result;
|
|
774
|
+
}
|
|
775
|
+
const timeline = collect(descriptor(
|
|
776
|
+
`repos/${repository}/issues/${number}/timeline`,
|
|
777
|
+
'full-enumeration',
|
|
778
|
+
'timeline',
|
|
779
|
+
(item) => timelineIdentity(item, number),
|
|
780
|
+
{ parentIdentity }
|
|
781
|
+
));
|
|
782
|
+
if (!timeline.ok) return timeline;
|
|
783
|
+
}
|
|
784
|
+
allObjects.push(...pageObjects, ...resources.values());
|
|
785
|
+
return {
|
|
786
|
+
ok: true,
|
|
787
|
+
value: {
|
|
788
|
+
repository,
|
|
789
|
+
preflightDate: preflightDate.value.iso,
|
|
790
|
+
watermark: watermark.toISOString(),
|
|
791
|
+
objects: allObjects,
|
|
792
|
+
endpoints,
|
|
793
|
+
responseDates: [...new Set(responseDates)].sort(),
|
|
794
|
+
deferred: [...deferred].sort(),
|
|
795
|
+
unavailable: [...unavailable].sort()
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export { collectGitHubBoundary, collectGitHubObjects, collectLocalObjects, fetchRestCollection, projectGitHubItem, projectGitHubTimelineItem, resolveGitHubRepository };
|
|
801
|
+
export type { BoundaryCaptureOptions, FetchRestCollectionOptions, GitHubBoundaryCapture, GitHubCapture, RestCollection };
|