@aiwg/cli 2026.10.10 → 2026.10.12
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/agentic/code/providers/model-capabilities.v1.json +78 -14
- package/agentic/code/providers/model-catalog.v1.json +62 -12
- package/dist/src/artifacts/binding-target-identity.js +51 -0
- package/dist/src/artifacts/checksum-manifest.js +12 -0
- package/dist/src/artifacts/index-builder.js +11 -13
- package/dist/src/artifacts/query-engine.js +5 -11
- package/dist/src/cli/handlers/doctor-help.js +32 -0
- package/dist/src/cli/handlers/gates.js +19 -18
- package/dist/src/cli/handlers/models-help.js +7 -1
- package/dist/src/cli/handlers/models.js +3 -3
- package/dist/src/cli/handlers/script-runner.js +8 -5
- package/dist/src/cli/handlers/sessions-intake.js +104 -0
- package/dist/src/cli/handlers/sessions-tail.js +172 -0
- package/dist/src/cli/handlers/sessions.js +287 -31
- package/dist/src/cli/handlers/skill-lint.js +10 -3
- package/dist/src/cli/handlers/steward-format.js +100 -0
- package/dist/src/cli/handlers/steward-models.js +167 -0
- package/dist/src/cli/handlers/steward.js +14 -248
- package/dist/src/cli/handlers/use-bundle-counts.js +61 -0
- package/dist/src/cli/handlers/use.js +18 -28
- package/dist/src/cli/handlers/utilities.js +1 -24
- package/dist/src/config/aiwg-config.js +7 -0
- package/dist/src/config/authorization-cli.js +89 -0
- package/dist/src/config/cli-help.js +1 -0
- package/dist/src/config/cli.js +10 -1
- package/dist/src/config/worker-context.js +337 -0
- package/dist/src/decision/context-pruning.js +3 -2
- package/dist/src/decision/ensemble/contract.js +15 -4
- package/dist/src/decision/ensemble-study/artifacts.js +5 -0
- package/dist/src/decision/ensemble-study/score.js +4 -3
- package/dist/src/decision/feature-export.js +3 -9
- package/dist/src/decision/heldout/collector.js +2 -2
- package/dist/src/decision/issue-triage/runtime.js +3 -9
- package/dist/src/decision/qualification/gates.js +2 -1
- package/dist/src/decision/qualification/release.js +2 -3
- package/dist/src/decision/routing/report.js +2 -0
- package/dist/src/decision/sdlc-screening.js +3 -5
- package/dist/src/decision/sensitivity/comparative.js +5 -3
- package/dist/src/gates/driver.js +6 -77
- package/dist/src/gates/evaluate.js +4 -11
- package/dist/src/gates/floors.js +4 -4
- package/dist/src/gates/index.js +3 -3
- package/dist/src/gates/lattice.js +26 -0
- package/dist/src/gates/registry.js +2 -1
- package/dist/src/gates/report.js +19 -7
- package/dist/src/issues/audit-cli.js +120 -0
- package/dist/src/issues/audit-contract.js +64 -0
- package/dist/src/issues/audit-local.js +82 -0
- package/dist/src/issues/audit.js +220 -0
- package/dist/src/lint/rule-activation.js +15 -1
- package/dist/src/mcp/tools/worker-invoke.mjs +108 -10
- package/dist/src/mission-protocol/canonical-codecs.js +270 -0
- package/dist/src/mission-protocol/codecs.js +22 -550
- package/dist/src/mission-protocol/health-trace-codecs.js +281 -0
- package/dist/src/mission-protocol/health-trace-types.js +43 -0
- package/dist/src/mission-protocol/types.js +2 -42
- package/dist/src/mission-protocol/worker-contract-json.js +10 -0
- package/dist/src/mission-protocol/worker-review-gate.js +100 -0
- package/dist/src/mission-protocol/worker-review-types.js +2 -0
- package/dist/src/models/catalog-policy.js +280 -0
- package/dist/src/models/index.js +1 -1
- package/dist/src/models/model-capabilities.v1.json +78 -14
- package/dist/src/models/model-catalog.v1.json +62 -12
- package/dist/src/models/policy-resources.js +28 -0
- package/dist/src/models/provider-policy.js +10 -539
- package/dist/src/models/resolver.js +26 -8
- package/dist/src/models/router.js +15 -2
- package/dist/src/models/tiers.js +13 -0
- package/dist/src/models/types.js +0 -6
- package/dist/src/models/worker-ceilings.js +59 -0
- package/dist/src/models/worker-invocation-policy.js +243 -0
- package/dist/src/models/worker-plan.js +16 -20
- package/dist/src/models/worker-review-lineage.js +55 -0
- package/dist/src/models/worker-selection-policy.js +1 -1
- package/dist/src/models/wrapper-route.js +6 -3
- package/dist/src/policy/authorization.js +30 -1
- package/dist/src/policy/hitl-config.js +63 -0
- package/dist/src/policy/hitl-receipt.js +57 -0
- package/dist/src/policy/hitl-types.js +4 -0
- package/dist/src/policy/hitl-validation.js +39 -0
- package/dist/src/policy/hitl.js +117 -0
- package/dist/src/process/aiwg-probe-command.d.mts +15 -0
- package/dist/src/process/aiwg-probe-command.mjs +37 -0
- package/dist/src/rules/activation.d.mts +1 -1
- package/dist/src/rules/activation.mjs +0 -2
- package/dist/src/rules/gate-coverage.js +51 -0
- package/dist/src/rules/gate-references.js +16 -0
- package/dist/src/rules/rule-frontmatter.schema.json +7 -1
- package/dist/src/serve/worker-command-request.js +21 -4
- package/dist/src/serve/worker-invocation-preparation.js +3 -1
- package/dist/src/serve/worker-invocation.js +21 -10
- package/dist/src/sessions/adapters/deepseek-harness.js +1 -0
- package/dist/src/sessions/adapters/grok-build-native.js +269 -0
- package/dist/src/sessions/adapters/grok-build.js +57 -15
- package/dist/src/sessions/adapters/grokbot.js +7 -0
- package/dist/src/sessions/adapters/hermes-state-db.js +252 -0
- package/dist/src/sessions/adapters/hermes.js +42 -3
- package/dist/src/sessions/adapters/warp.js +1 -1
- package/dist/src/sessions/batch-import.js +44 -6
- package/dist/src/sessions/contracts.js +20 -1
- package/dist/src/sessions/coverage-preflight.js +7 -1
- package/dist/src/sessions/discovery-capability.js +38 -0
- package/dist/src/sessions/fortemi-export-mapping.js +33 -3
- package/dist/src/sessions/host-catalog.js +157 -0
- package/dist/src/sessions/image-blobs.js +141 -0
- package/dist/src/sessions/importer.js +9 -232
- package/dist/src/sessions/index.js +6 -0
- package/dist/src/sessions/intake-dir.js +71 -0
- package/dist/src/sessions/normalization.js +251 -0
- package/dist/src/sessions/refresh.js +25 -2
- package/dist/src/sessions/repository.js +164 -22
- package/dist/src/sessions/tail.js +91 -0
- package/dist/src/sessions/workspace-discovery.js +88 -9
- package/dist/src/skills/agent-skill-sidecar.js +28 -0
- package/dist/src/skills/agent-skills.js +4 -23
- package/dist/src/skills/claude-surface-projection.js +25 -0
- package/dist/src/skills/deployer.js +8 -288
- package/dist/src/skills/deployment-policy.js +114 -0
- package/dist/src/skills/deployment-projection.js +152 -0
- package/dist/src/skills/deployment-types.js +20 -0
- package/dist/src/skills/exporter.js +8 -31
- package/dist/src/skills/provider-extensions.js +41 -0
- package/dist/src/skills/run.js +6 -0
- package/dist/src/skills/types.js +0 -8
- package/dist/src/skills/validator-resources.js +68 -0
- package/dist/src/skills/validator.js +6 -55
- package/dist/src/storage/cli-migrate-options.js +125 -0
- package/dist/src/storage/cli-migrate.js +111 -0
- package/dist/src/storage/cli.js +4 -279
- package/dist/src/storage/migration-adapter.js +79 -0
- package/dist/src/storage/migration-copy.js +108 -0
- package/dist/src/storage/migration-protocol.js +14 -197
- package/dist/src/storage/migration-records.js +186 -0
- package/dist/src/storage/migration-types.js +12 -0
- package/dist/src/tracker/forge-http.js +2 -1
- package/dist/src/tracker/issue-pages.js +110 -0
- package/package.json +7 -1
- package/schemas/mission-v1.schema.json +335 -0
- package/schemas/models/model-catalog.v1.schema.json +19 -1
- package/schemas/models/model-config-compatibility.v1.schema.json +3 -2
- package/schemas/models/model-policy.v1.schema.json +1 -1
- package/tools/agents/providers/provider-paths-staging.mjs +35 -0
- package/tools/agents/providers/rule-binding-targets.mjs +146 -0
- package/tools/agents/providers/rule-delivery.mjs +7 -126
- package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
- package/tools/ralph-external/lib/worker-context.mjs +237 -0
- package/tools/ralph-external/lib/worker-invocation-ancestry.mjs +132 -0
- package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
- package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
- package/tools/ralph-external/lib/worker-isolation.mjs +17 -29
- package/tools/ralph-external/lib/worker-launch-cli.mjs +3 -1
- package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
- package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
- package/tools/ralph-external/lib/worker-termination.mjs +82 -0
- package/tools/ralph-external/session-launcher.mjs +51 -96
- package/vscode-extension/schemas/aiwg.config.v1.json +177 -0
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
this.code = code;
|
|
9
|
-
this.retryable = retryable;
|
|
10
|
-
this.name = 'MigrationProtocolError';
|
|
11
|
-
}
|
|
12
|
-
}
|
|
1
|
+
// Coordinates protocol migration, copy checkpoints and backend cutover.
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { MigrationProtocolError, STORAGE_MIGRATION_PROTOCOL, } from './migration-types.js';
|
|
4
|
+
import { boundedInteger, digestRecords, assertBoundaryState, validateManifest, summarizeVerificationFailure, sha256, stableStringify, approvalDigest, recordMap, digestRecordChunks, validateSemanticVerification, semanticIsValid, receiptKey, identityKey, recordKey, compareRevisions, revisionConflict, chunk, commitMigrationRecords, isRetryableError, errorMessage, abortableDelay, } from './migration-records.js';
|
|
5
|
+
import { runMigrationCopy } from './migration-copy.js';
|
|
6
|
+
export * from './migration-types.js';
|
|
7
|
+
export { digestRecords, digestRecordChunks, approvalDigest, validateManifest } from './migration-records.js';
|
|
13
8
|
export class StorageMigrationCoordinator {
|
|
14
9
|
source;
|
|
15
10
|
destination;
|
|
16
11
|
store;
|
|
17
12
|
routing;
|
|
18
13
|
options;
|
|
14
|
+
/** Copy-only CLI workflow: digest-aware resume and plan-authorized deletes, without cutover. */
|
|
15
|
+
static copy(request) {
|
|
16
|
+
return runMigrationCopy(request);
|
|
17
|
+
}
|
|
19
18
|
batchSize;
|
|
20
19
|
concurrency;
|
|
21
20
|
maxRetries;
|
|
@@ -303,33 +302,11 @@ export class StorageMigrationCoordinator {
|
|
|
303
302
|
if (index >= batches.length)
|
|
304
303
|
return;
|
|
305
304
|
const batch = batches[index];
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
if (!receipt.committed || receipt.recordReceipts.length !== batch.length) {
|
|
309
|
-
throw new MigrationProtocolError('AIWG_MIGRATION_INVALID_RECEIPT', `destination returned an invalid receipt for batch ${index}`);
|
|
310
|
-
}
|
|
311
|
-
const committedAt = this.now().toISOString();
|
|
312
|
-
const committedByIdentity = new Map(receipt.recordReceipts.map(recordReceipt => [identityKey(recordReceipt.identity), recordReceipt]));
|
|
313
|
-
for (let recordIndex = 0; recordIndex < batch.length; recordIndex++) {
|
|
314
|
-
const record = batch[recordIndex];
|
|
315
|
-
const committed = committedByIdentity.get(identityKey(record.identity));
|
|
316
|
-
if (!committed || committed.digest !== record.digest) {
|
|
317
|
-
throw new MigrationProtocolError('AIWG_MIGRATION_CORRUPT_RECEIPT', `destination receipt diverged for ${identityKey(record.identity)}`);
|
|
318
|
-
}
|
|
319
|
-
manifest.records.push({
|
|
320
|
-
identity: record.identity,
|
|
321
|
-
sourceRevision: record.sourceRevision,
|
|
322
|
-
contentDigest: record.digest,
|
|
323
|
-
destinationRevision: committed.sourceRevision,
|
|
324
|
-
tombstone: Boolean(record.tombstone),
|
|
325
|
-
idempotencyKey: mutations[recordIndex].idempotencyKey,
|
|
326
|
-
batchId: receipt.batchId,
|
|
327
|
-
committedAt,
|
|
328
|
-
});
|
|
329
|
-
}
|
|
305
|
+
const committed = await commitMigrationRecords(manifest.migrationId, batch, record => latest.get(identityKey(record.identity))?.destinationRevision, mutations => this.retry(() => this.destination.commitBatch(mutations, this.options.signal), `batch ${index}`), this.now);
|
|
306
|
+
manifest.records.push(...committed.records);
|
|
330
307
|
manifest.counts.committed = manifest.records.length;
|
|
331
308
|
manifest.checkpoint.committedBatches += 1;
|
|
332
|
-
manifest.checkpoint.highWaterMark =
|
|
309
|
+
manifest.checkpoint.highWaterMark = committed.batch.highWaterMark;
|
|
333
310
|
await this.persist(manifest);
|
|
334
311
|
}
|
|
335
312
|
});
|
|
@@ -393,164 +370,4 @@ export class StorageMigrationCoordinator {
|
|
|
393
370
|
}
|
|
394
371
|
}
|
|
395
372
|
}
|
|
396
|
-
export function digestRecords(records) {
|
|
397
|
-
const normalized = [...records]
|
|
398
|
-
.map(record => ({
|
|
399
|
-
identity: record.identity,
|
|
400
|
-
sourceRevision: record.sourceRevision,
|
|
401
|
-
digest: record.digest,
|
|
402
|
-
tombstone: record.tombstone ?? null,
|
|
403
|
-
}))
|
|
404
|
-
.sort((left, right) => identityKey(left.identity).localeCompare(identityKey(right.identity)));
|
|
405
|
-
return sha256(stableStringify(normalized));
|
|
406
|
-
}
|
|
407
|
-
export function digestRecordChunks(records, chunkSize) {
|
|
408
|
-
const sorted = [...records].sort((left, right) => identityKey(left.identity).localeCompare(identityKey(right.identity)));
|
|
409
|
-
return chunk(sorted, chunkSize).map(recordsInChunk => digestRecords(recordsInChunk));
|
|
410
|
-
}
|
|
411
|
-
export function approvalDigest(manifest) {
|
|
412
|
-
const copy = structuredClone(manifest);
|
|
413
|
-
delete copy.digests.approval;
|
|
414
|
-
return sha256(stableStringify(copy));
|
|
415
|
-
}
|
|
416
|
-
export function validateManifest(manifest, source, destination) {
|
|
417
|
-
if (manifest.protocol !== STORAGE_MIGRATION_PROTOCOL) {
|
|
418
|
-
throw new MigrationProtocolError('AIWG_MIGRATION_PROTOCOL_UNSUPPORTED', `unsupported migration protocol ${String(manifest.protocol)}`);
|
|
419
|
-
}
|
|
420
|
-
if (!manifest.migrationId || !manifest.snapshot?.id || !manifest.digests?.source) {
|
|
421
|
-
throw new MigrationProtocolError('AIWG_MIGRATION_MANIFEST_CORRUPT', 'migration manifest is missing required identity or digest fields');
|
|
422
|
-
}
|
|
423
|
-
if (!manifest.boundaries?.initial) {
|
|
424
|
-
throw new MigrationProtocolError('AIWG_MIGRATION_BOUNDARY_MISSING', 'migration manifest has no durable source boundary receipt');
|
|
425
|
-
}
|
|
426
|
-
assertBoundaryState(manifest.boundaries.initial, manifest.mode === 'offline' ? 'quiesced' : 'tracking', 'initial');
|
|
427
|
-
if (source && stableStringify(manifest.source) !== stableStringify(source)) {
|
|
428
|
-
throw new MigrationProtocolError('AIWG_MIGRATION_SOURCE_MISMATCH', 'manifest source does not match the active source');
|
|
429
|
-
}
|
|
430
|
-
if (destination && stableStringify(manifest.destination) !== stableStringify(destination)) {
|
|
431
|
-
throw new MigrationProtocolError('AIWG_MIGRATION_DESTINATION_MISMATCH', 'manifest destination does not match the active destination');
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
function assertBoundaryState(boundary, expected, stage) {
|
|
435
|
-
if (!boundary.boundaryId || !boundary.establishedAt || boundary.state !== expected) {
|
|
436
|
-
throw new MigrationProtocolError('AIWG_MIGRATION_BOUNDARY_INVALID', `${stage} source boundary must provide a durable ${expected} receipt`);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
function mutationFor(migrationId, record, expectedRevision) {
|
|
440
|
-
return {
|
|
441
|
-
operation: record.tombstone ? 'delete' : 'upsert',
|
|
442
|
-
record,
|
|
443
|
-
idempotencyKey: sha256(`${migrationId}\0${recordKey(record)}`),
|
|
444
|
-
...(expectedRevision === undefined ? {} : { expectedRevision }),
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
function revisionConflict(identity, revision) {
|
|
448
|
-
return new MigrationProtocolError('AIWG_MIGRATION_REVISION_CONFLICT', `source revision ${revision} has conflicting content for ${identity}`);
|
|
449
|
-
}
|
|
450
|
-
function compareRevisions(comparator, left, right) {
|
|
451
|
-
const result = comparator(left, right);
|
|
452
|
-
if (!Number.isFinite(result)) {
|
|
453
|
-
throw new MigrationProtocolError('AIWG_MIGRATION_REVISION_ORDER_INVALID', 'source-revision comparator returned a non-finite result');
|
|
454
|
-
}
|
|
455
|
-
return Math.sign(result);
|
|
456
|
-
}
|
|
457
|
-
function recordMap(records) {
|
|
458
|
-
return new Map(records.map(record => [identityKey(record.identity), record]));
|
|
459
|
-
}
|
|
460
|
-
function identityKey(identity) {
|
|
461
|
-
return `${identity.tenant}\0${identity.subsystem}\0${identity.path}`;
|
|
462
|
-
}
|
|
463
|
-
function recordKey(record) {
|
|
464
|
-
return `${identityKey(record.identity)}\0${record.sourceRevision}\0${record.digest}`;
|
|
465
|
-
}
|
|
466
|
-
function receiptKey(receipt) {
|
|
467
|
-
return `${identityKey(receipt.identity)}\0${receipt.sourceRevision}\0${receipt.contentDigest}`;
|
|
468
|
-
}
|
|
469
|
-
function sha256(value) {
|
|
470
|
-
return createHash('sha256').update(value).digest('hex');
|
|
471
|
-
}
|
|
472
|
-
function stableStringify(value) {
|
|
473
|
-
if (Array.isArray(value))
|
|
474
|
-
return `[${value.map(stableStringify).join(',')}]`;
|
|
475
|
-
if (value && typeof value === 'object') {
|
|
476
|
-
return `{${Object.entries(value)
|
|
477
|
-
.filter(([, child]) => child !== undefined)
|
|
478
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
479
|
-
.map(([key, child]) => `${JSON.stringify(key)}:${stableStringify(child)}`)
|
|
480
|
-
.join(',')}}`;
|
|
481
|
-
}
|
|
482
|
-
return JSON.stringify(value);
|
|
483
|
-
}
|
|
484
|
-
function chunk(items, size) {
|
|
485
|
-
const chunks = [];
|
|
486
|
-
for (let index = 0; index < items.length; index += size)
|
|
487
|
-
chunks.push(items.slice(index, index + size));
|
|
488
|
-
return chunks;
|
|
489
|
-
}
|
|
490
|
-
function boundedInteger(raw, fallback, min, max, label) {
|
|
491
|
-
const value = raw ?? fallback;
|
|
492
|
-
if (!Number.isInteger(value) || value < min || value > max) {
|
|
493
|
-
throw new MigrationProtocolError('AIWG_MIGRATION_INVALID_OPTION', `${label} must be an integer from ${min} through ${max}`);
|
|
494
|
-
}
|
|
495
|
-
return value;
|
|
496
|
-
}
|
|
497
|
-
function summarizeVerificationFailure(verification) {
|
|
498
|
-
const semanticFailures = Object.values(verification.semantic).filter(check => !check.valid).length;
|
|
499
|
-
return `migration parity failed: missing=${verification.missing.length} unexpected=${verification.unexpected.length} corrupt=${verification.corrupt.length} chunks=${verification.chunks.valid ? 'valid' : 'invalid'} semantic=${semanticFailures} lag=${verification.lag}`;
|
|
500
|
-
}
|
|
501
|
-
function validateSemanticVerification(verification) {
|
|
502
|
-
for (const dimension of [
|
|
503
|
-
'schema', 'constraints', 'countsByType', 'edgeIntegrity', 'queryParity', 'traversalParity',
|
|
504
|
-
]) {
|
|
505
|
-
const check = verification?.[dimension];
|
|
506
|
-
if (!check || typeof check.valid !== 'boolean' || !Number.isInteger(check.declared) || check.declared < 0 ||
|
|
507
|
-
!Number.isInteger(check.checked) || check.checked < 0 || check.checked !== check.declared || !Array.isArray(check.failures)) {
|
|
508
|
-
throw new MigrationProtocolError('AIWG_MIGRATION_SEMANTIC_VERIFICATION_INVALID', `semantic verification is incomplete for ${dimension}`);
|
|
509
|
-
}
|
|
510
|
-
if (check.valid !== (check.failures.length === 0)) {
|
|
511
|
-
throw new MigrationProtocolError('AIWG_MIGRATION_SEMANTIC_VERIFICATION_INVALID', `semantic verification validity disagrees with failures for ${dimension}`);
|
|
512
|
-
}
|
|
513
|
-
if (check.evidenceDigest !== undefined && !/^[0-9A-Za-z._:-]+$/.test(check.evidenceDigest)) {
|
|
514
|
-
throw new MigrationProtocolError('AIWG_MIGRATION_SEMANTIC_VERIFICATION_INVALID', `semantic verification evidence digest is invalid for ${dimension}`);
|
|
515
|
-
}
|
|
516
|
-
if (['schema', 'constraints', 'queryParity'].includes(dimension) && check.declared < 1) {
|
|
517
|
-
throw new MigrationProtocolError('AIWG_MIGRATION_SEMANTIC_VERIFICATION_INVALID', `semantic verification must exercise at least one ${dimension} check`);
|
|
518
|
-
}
|
|
519
|
-
if (dimension === 'countsByType') {
|
|
520
|
-
if (!isCountMap(check.sourceCounts) || !isCountMap(check.destinationCounts)) {
|
|
521
|
-
throw new MigrationProtocolError('AIWG_MIGRATION_SEMANTIC_VERIFICATION_INVALID', 'countsByType must include non-negative integer source and destination maps');
|
|
522
|
-
}
|
|
523
|
-
const declaredTypes = new Set([...Object.keys(check.sourceCounts), ...Object.keys(check.destinationCounts)]).size;
|
|
524
|
-
if (check.declared !== declaredTypes || (check.valid && stableStringify(check.sourceCounts) !== stableStringify(check.destinationCounts))) {
|
|
525
|
-
throw new MigrationProtocolError('AIWG_MIGRATION_SEMANTIC_VERIFICATION_INVALID', 'countsByType scope or validity does not match its declared maps');
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
function semanticIsValid(verification) {
|
|
531
|
-
return Object.values(verification).every(check => check.valid);
|
|
532
|
-
}
|
|
533
|
-
function isCountMap(value) {
|
|
534
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value) &&
|
|
535
|
-
Object.values(value).every(count => Number.isInteger(count) && count >= 0);
|
|
536
|
-
}
|
|
537
|
-
function errorMessage(error) {
|
|
538
|
-
return error instanceof Error ? error.message : String(error);
|
|
539
|
-
}
|
|
540
|
-
function isRetryableError(error) {
|
|
541
|
-
return typeof error === 'object' && error !== null && 'retryable' in error && error.retryable === true;
|
|
542
|
-
}
|
|
543
|
-
async function abortableDelay(ms, signal) {
|
|
544
|
-
if (!signal) {
|
|
545
|
-
await new Promise(resolve => setTimeout(resolve, ms));
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
await new Promise((resolve, reject) => {
|
|
549
|
-
const timer = setTimeout(resolve, ms);
|
|
550
|
-
signal.addEventListener('abort', () => {
|
|
551
|
-
clearTimeout(timer);
|
|
552
|
-
reject(new MigrationProtocolError('AIWG_MIGRATION_CANCELLED', 'migration cancelled by operator'));
|
|
553
|
-
}, { once: true });
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
373
|
//# sourceMappingURL=migration-protocol.js.map
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Record digests, validation and retry helpers for storage migration.
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { STORAGE_MIGRATION_PROTOCOL, MigrationProtocolError, } from './migration-types.js';
|
|
4
|
+
export function digestRecords(records) {
|
|
5
|
+
const normalized = [...records]
|
|
6
|
+
.map(record => ({
|
|
7
|
+
identity: record.identity,
|
|
8
|
+
sourceRevision: record.sourceRevision,
|
|
9
|
+
digest: record.digest,
|
|
10
|
+
tombstone: record.tombstone ?? null,
|
|
11
|
+
}))
|
|
12
|
+
.sort((left, right) => identityKey(left.identity).localeCompare(identityKey(right.identity)));
|
|
13
|
+
return sha256(stableStringify(normalized));
|
|
14
|
+
}
|
|
15
|
+
export function digestRecordChunks(records, chunkSize) {
|
|
16
|
+
const sorted = [...records].sort((left, right) => identityKey(left.identity).localeCompare(identityKey(right.identity)));
|
|
17
|
+
return chunk(sorted, chunkSize).map(recordsInChunk => digestRecords(recordsInChunk));
|
|
18
|
+
}
|
|
19
|
+
export function approvalDigest(manifest) {
|
|
20
|
+
const copy = structuredClone(manifest);
|
|
21
|
+
delete copy.digests.approval;
|
|
22
|
+
return sha256(stableStringify(copy));
|
|
23
|
+
}
|
|
24
|
+
export function validateManifest(manifest, source, destination) {
|
|
25
|
+
if (manifest.protocol !== STORAGE_MIGRATION_PROTOCOL) {
|
|
26
|
+
throw new MigrationProtocolError('AIWG_MIGRATION_PROTOCOL_UNSUPPORTED', `unsupported migration protocol ${String(manifest.protocol)}`);
|
|
27
|
+
}
|
|
28
|
+
if (!manifest.migrationId || !manifest.snapshot?.id || !manifest.digests?.source) {
|
|
29
|
+
throw new MigrationProtocolError('AIWG_MIGRATION_MANIFEST_CORRUPT', 'migration manifest is missing required identity or digest fields');
|
|
30
|
+
}
|
|
31
|
+
if (!manifest.boundaries?.initial) {
|
|
32
|
+
throw new MigrationProtocolError('AIWG_MIGRATION_BOUNDARY_MISSING', 'migration manifest has no durable source boundary receipt');
|
|
33
|
+
}
|
|
34
|
+
assertBoundaryState(manifest.boundaries.initial, manifest.mode === 'offline' ? 'quiesced' : 'tracking', 'initial');
|
|
35
|
+
if (source && stableStringify(manifest.source) !== stableStringify(source)) {
|
|
36
|
+
throw new MigrationProtocolError('AIWG_MIGRATION_SOURCE_MISMATCH', 'manifest source does not match the active source');
|
|
37
|
+
}
|
|
38
|
+
if (destination && stableStringify(manifest.destination) !== stableStringify(destination)) {
|
|
39
|
+
throw new MigrationProtocolError('AIWG_MIGRATION_DESTINATION_MISMATCH', 'manifest destination does not match the active destination');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function assertBoundaryState(boundary, expected, stage) {
|
|
43
|
+
if (!boundary.boundaryId || !boundary.establishedAt || boundary.state !== expected) {
|
|
44
|
+
throw new MigrationProtocolError('AIWG_MIGRATION_BOUNDARY_INVALID', `${stage} source boundary must provide a durable ${expected} receipt`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function mutationFor(migrationId, record, expectedRevision) {
|
|
48
|
+
return {
|
|
49
|
+
operation: record.tombstone ? 'delete' : 'upsert',
|
|
50
|
+
record,
|
|
51
|
+
idempotencyKey: sha256(`${migrationId}\0${recordKey(record)}`),
|
|
52
|
+
...(expectedRevision === undefined ? {} : { expectedRevision }),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function revisionConflict(identity, revision) {
|
|
56
|
+
return new MigrationProtocolError('AIWG_MIGRATION_REVISION_CONFLICT', `source revision ${revision} has conflicting content for ${identity}`);
|
|
57
|
+
}
|
|
58
|
+
export function compareRevisions(comparator, left, right) {
|
|
59
|
+
const result = comparator(left, right);
|
|
60
|
+
if (!Number.isFinite(result)) {
|
|
61
|
+
throw new MigrationProtocolError('AIWG_MIGRATION_REVISION_ORDER_INVALID', 'source-revision comparator returned a non-finite result');
|
|
62
|
+
}
|
|
63
|
+
return Math.sign(result);
|
|
64
|
+
}
|
|
65
|
+
export function recordMap(records) {
|
|
66
|
+
return new Map(records.map(record => [identityKey(record.identity), record]));
|
|
67
|
+
}
|
|
68
|
+
export function identityKey(identity) {
|
|
69
|
+
return `${identity.tenant}\0${identity.subsystem}\0${identity.path}`;
|
|
70
|
+
}
|
|
71
|
+
export function recordKey(record) {
|
|
72
|
+
return `${identityKey(record.identity)}\0${record.sourceRevision}\0${record.digest}`;
|
|
73
|
+
}
|
|
74
|
+
export function receiptKey(receipt) {
|
|
75
|
+
return `${identityKey(receipt.identity)}\0${receipt.sourceRevision}\0${receipt.contentDigest}`;
|
|
76
|
+
}
|
|
77
|
+
export function sha256(value) {
|
|
78
|
+
return createHash('sha256').update(value).digest('hex');
|
|
79
|
+
}
|
|
80
|
+
export function stableStringify(value) {
|
|
81
|
+
if (Array.isArray(value))
|
|
82
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
83
|
+
if (value && typeof value === 'object') {
|
|
84
|
+
return `{${Object.entries(value)
|
|
85
|
+
.filter(([, child]) => child !== undefined)
|
|
86
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
87
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${stableStringify(child)}`)
|
|
88
|
+
.join(',')}}`;
|
|
89
|
+
}
|
|
90
|
+
return JSON.stringify(value);
|
|
91
|
+
}
|
|
92
|
+
export function chunk(items, size) {
|
|
93
|
+
const chunks = [];
|
|
94
|
+
for (let index = 0; index < items.length; index += size)
|
|
95
|
+
chunks.push(items.slice(index, index + size));
|
|
96
|
+
return chunks;
|
|
97
|
+
}
|
|
98
|
+
export function boundedInteger(raw, fallback, min, max, label) {
|
|
99
|
+
const value = raw ?? fallback;
|
|
100
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
101
|
+
throw new MigrationProtocolError('AIWG_MIGRATION_INVALID_OPTION', `${label} must be an integer from ${min} through ${max}`);
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
export function summarizeVerificationFailure(verification) {
|
|
106
|
+
const semanticFailures = Object.values(verification.semantic).filter(check => !check.valid).length;
|
|
107
|
+
return `migration parity failed: missing=${verification.missing.length} unexpected=${verification.unexpected.length} corrupt=${verification.corrupt.length} chunks=${verification.chunks.valid ? 'valid' : 'invalid'} semantic=${semanticFailures} lag=${verification.lag}`;
|
|
108
|
+
}
|
|
109
|
+
export function validateSemanticVerification(verification) {
|
|
110
|
+
for (const dimension of [
|
|
111
|
+
'schema', 'constraints', 'countsByType', 'edgeIntegrity', 'queryParity', 'traversalParity',
|
|
112
|
+
]) {
|
|
113
|
+
const check = verification?.[dimension];
|
|
114
|
+
if (!check || typeof check.valid !== 'boolean' || !Number.isInteger(check.declared) || check.declared < 0 ||
|
|
115
|
+
!Number.isInteger(check.checked) || check.checked < 0 || check.checked !== check.declared || !Array.isArray(check.failures)) {
|
|
116
|
+
throw new MigrationProtocolError('AIWG_MIGRATION_SEMANTIC_VERIFICATION_INVALID', `semantic verification is incomplete for ${dimension}`);
|
|
117
|
+
}
|
|
118
|
+
if (check.valid !== (check.failures.length === 0)) {
|
|
119
|
+
throw new MigrationProtocolError('AIWG_MIGRATION_SEMANTIC_VERIFICATION_INVALID', `semantic verification validity disagrees with failures for ${dimension}`);
|
|
120
|
+
}
|
|
121
|
+
if (check.evidenceDigest !== undefined && !/^[0-9A-Za-z._:-]+$/.test(check.evidenceDigest)) {
|
|
122
|
+
throw new MigrationProtocolError('AIWG_MIGRATION_SEMANTIC_VERIFICATION_INVALID', `semantic verification evidence digest is invalid for ${dimension}`);
|
|
123
|
+
}
|
|
124
|
+
if (['schema', 'constraints', 'queryParity'].includes(dimension) && check.declared < 1) {
|
|
125
|
+
throw new MigrationProtocolError('AIWG_MIGRATION_SEMANTIC_VERIFICATION_INVALID', `semantic verification must exercise at least one ${dimension} check`);
|
|
126
|
+
}
|
|
127
|
+
if (dimension === 'countsByType') {
|
|
128
|
+
if (!isCountMap(check.sourceCounts) || !isCountMap(check.destinationCounts)) {
|
|
129
|
+
throw new MigrationProtocolError('AIWG_MIGRATION_SEMANTIC_VERIFICATION_INVALID', 'countsByType must include non-negative integer source and destination maps');
|
|
130
|
+
}
|
|
131
|
+
const declaredTypes = new Set([...Object.keys(check.sourceCounts), ...Object.keys(check.destinationCounts)]).size;
|
|
132
|
+
if (check.declared !== declaredTypes || (check.valid && stableStringify(check.sourceCounts) !== stableStringify(check.destinationCounts))) {
|
|
133
|
+
throw new MigrationProtocolError('AIWG_MIGRATION_SEMANTIC_VERIFICATION_INVALID', 'countsByType scope or validity does not match its declared maps');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export function semanticIsValid(verification) {
|
|
139
|
+
return Object.values(verification).every(check => check.valid);
|
|
140
|
+
}
|
|
141
|
+
export function isCountMap(value) {
|
|
142
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) &&
|
|
143
|
+
Object.values(value).every(count => Number.isInteger(count) && count >= 0);
|
|
144
|
+
}
|
|
145
|
+
export function errorMessage(error) {
|
|
146
|
+
return error instanceof Error ? error.message : String(error);
|
|
147
|
+
}
|
|
148
|
+
export function isRetryableError(error) {
|
|
149
|
+
return typeof error === 'object' && error !== null && 'retryable' in error && error.retryable === true;
|
|
150
|
+
}
|
|
151
|
+
export async function abortableDelay(ms, signal) {
|
|
152
|
+
if (!signal) {
|
|
153
|
+
await new Promise(resolve => setTimeout(resolve, ms));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
await new Promise((resolve, reject) => {
|
|
157
|
+
const timer = setTimeout(resolve, ms);
|
|
158
|
+
signal.addEventListener('abort', () => {
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
reject(new MigrationProtocolError('AIWG_MIGRATION_CANCELLED', 'migration cancelled by operator'));
|
|
161
|
+
}, { once: true });
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/** Shared batch mutation and receipt validation for copy-only and cutover migrations. */
|
|
165
|
+
export async function commitMigrationRecords(migrationId, records, expectedRevision, commit, now) {
|
|
166
|
+
const mutations = records.map(record => mutationFor(migrationId, record, expectedRevision(record)));
|
|
167
|
+
const batch = await commit(mutations);
|
|
168
|
+
if (!batch.committed || batch.recordReceipts.length !== records.length) {
|
|
169
|
+
throw new MigrationProtocolError('AIWG_MIGRATION_INVALID_RECEIPT', 'destination returned an invalid batch receipt');
|
|
170
|
+
}
|
|
171
|
+
const committedByIdentity = new Map(batch.recordReceipts.map(receipt => [identityKey(receipt.identity), receipt]));
|
|
172
|
+
const committedAt = now().toISOString();
|
|
173
|
+
const receipts = records.map((record, index) => {
|
|
174
|
+
const committed = committedByIdentity.get(identityKey(record.identity));
|
|
175
|
+
if (!committed || committed.digest !== record.digest) {
|
|
176
|
+
throw new MigrationProtocolError('AIWG_MIGRATION_CORRUPT_RECEIPT', `destination receipt diverged for ${identityKey(record.identity)}`);
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
identity: record.identity, sourceRevision: record.sourceRevision, contentDigest: record.digest,
|
|
180
|
+
destinationRevision: committed.sourceRevision, tombstone: Boolean(record.tombstone),
|
|
181
|
+
idempotencyKey: mutations[index].idempotencyKey, batchId: batch.batchId, committedAt,
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
return { batch, records: receipts };
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=migration-records.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const STORAGE_MIGRATION_PROTOCOL = 'aiwg.storage-migration/v1';
|
|
2
|
+
export class MigrationProtocolError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
retryable;
|
|
5
|
+
constructor(code, message, retryable = false) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.retryable = retryable;
|
|
9
|
+
this.name = 'MigrationProtocolError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=migration-types.js.map
|
|
@@ -202,7 +202,7 @@ export function classifyStatus(response) {
|
|
|
202
202
|
return 'server-error';
|
|
203
203
|
return 'unexpected-status';
|
|
204
204
|
}
|
|
205
|
-
function
|
|
205
|
+
export function parseForgeJson(response) {
|
|
206
206
|
const failure = classifyStatus(response);
|
|
207
207
|
if (failure)
|
|
208
208
|
throw new ForgeReadError(failure, response.status);
|
|
@@ -213,6 +213,7 @@ function parseJson(response) {
|
|
|
213
213
|
throw new ForgeReadError('malformed');
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
|
+
const parseJson = parseForgeJson;
|
|
216
217
|
/**
|
|
217
218
|
* Pagination from the provider headers (`x-hasmore`, `link rel="next"`,
|
|
218
219
|
* `x-total-count` / `x-total`). A full page with no pagination signal is
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Cursor-paged, metadata-only backlog views over the existing bounded forge transport.
|
|
2
|
+
import { createForgeReader, parseForgeJson, forgePagination, ForgeReadError, } from './forge-http.js';
|
|
3
|
+
import { AuditStop, auditDigest, boundedPageSize, decodeAuditCursor, encodeAuditCursor, parseAuditFilter, } from '../issues/audit-contract.js';
|
|
4
|
+
export function createForgeAuditTracker(provider, repository, transport, identity = `${provider}:${repository}`) {
|
|
5
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(repository) || repository.split('/').some(p => p === '.' || p === '..')) {
|
|
6
|
+
throw new AuditStop('tracker-repository-invalid');
|
|
7
|
+
}
|
|
8
|
+
const root = `/repos/${repository.split('/').map(encodeURIComponent).join('/')}/issues`;
|
|
9
|
+
const sizeKey = provider === 'github' ? 'per_page' : 'limit';
|
|
10
|
+
const object = (raw) => {
|
|
11
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
12
|
+
throw new ForgeReadError('malformed');
|
|
13
|
+
return raw;
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
identity,
|
|
17
|
+
async listMetadata(options) {
|
|
18
|
+
const size = boundedPageSize(options.pageSize);
|
|
19
|
+
const filter = parseAuditFilter(options.filter);
|
|
20
|
+
const scope = auditDigest([identity, size, filter]);
|
|
21
|
+
const cursor = options.cursor ? decodeAuditCursor(options.cursor) : null;
|
|
22
|
+
if (options.cursor && (!cursor || typeof cursor !== 'object'))
|
|
23
|
+
throw new AuditStop('cursor-invalidated');
|
|
24
|
+
if (cursor && (cursor.scope !== scope || !Number.isSafeInteger(cursor.page) || cursor.page < 2
|
|
25
|
+
|| typeof cursor.previous !== 'string'))
|
|
26
|
+
throw new AuditStop('cursor-invalidated');
|
|
27
|
+
const read = async (page) => {
|
|
28
|
+
const query = new URLSearchParams({ state: filter.status, sort: provider === 'gitea' ? 'oldest' : 'created', direction: 'asc',
|
|
29
|
+
page: String(page), [sizeKey]: String(size), ...(provider === 'gitea' ? { type: 'issues' } : {}) });
|
|
30
|
+
if (filter.labels.length)
|
|
31
|
+
query.set('labels', filter.labels.join(','));
|
|
32
|
+
const response = await transport(`${root}?${query}`, options.signal);
|
|
33
|
+
if (response.body.length > options.maxBytes)
|
|
34
|
+
throw new AuditStop('byte-budget');
|
|
35
|
+
const raw = parseForgeJson(response);
|
|
36
|
+
if (!Array.isArray(raw) || raw.length > size)
|
|
37
|
+
throw new ForgeReadError('malformed');
|
|
38
|
+
const records = raw.map(object);
|
|
39
|
+
const items = records.filter(r => !r.pull_request).map(r => {
|
|
40
|
+
const number = r.number ?? r.index;
|
|
41
|
+
if (!Number.isSafeInteger(number) || Number(number) < 1 || typeof r.title !== 'string'
|
|
42
|
+
|| typeof r.updated_at !== 'string' || (r.state !== 'open' && r.state !== 'closed'))
|
|
43
|
+
throw new ForgeReadError('malformed');
|
|
44
|
+
return { id: String(number), title: r.title, status: r.state, updatedAt: r.updated_at,
|
|
45
|
+
labels: Array.isArray(r.labels) ? r.labels.map(l => typeof l === 'string' ? l : String(object(l).name ?? '')) : [] };
|
|
46
|
+
});
|
|
47
|
+
// Fingerprint includes PR positions, but excludes bodies/comments discarded at this boundary.
|
|
48
|
+
const fingerprint = auditDigest(records.map(r => [r.number ?? r.index, r.updated_at, r.state, r.title, r.labels]));
|
|
49
|
+
const pagination = forgePagination(response.headers, raw, page, size);
|
|
50
|
+
return { items, fingerprint, pagination, bytes: response.body.length };
|
|
51
|
+
};
|
|
52
|
+
let verifiedBytes = 0;
|
|
53
|
+
if (cursor) {
|
|
54
|
+
// Offset APIs cannot guarantee a snapshot. Refuse shifted/edited boundaries instead of claiming completeness.
|
|
55
|
+
const previous = await read(cursor.page - 1);
|
|
56
|
+
verifiedBytes = previous.bytes;
|
|
57
|
+
if (previous.fingerprint !== cursor.previous || previous.pagination.totalCount !== cursor.total) {
|
|
58
|
+
throw new AuditStop('tracker-changed');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const pageNumber = cursor?.page ?? 1;
|
|
62
|
+
const page = await read(pageNumber);
|
|
63
|
+
if (cursor && page.pagination.totalCount !== cursor.total)
|
|
64
|
+
throw new AuditStop('tracker-changed');
|
|
65
|
+
if (page.bytes + verifiedBytes > options.maxBytes)
|
|
66
|
+
throw new AuditStop('byte-budget');
|
|
67
|
+
// A full page lacking pagination headers needs one bounded probe, never a false complete result.
|
|
68
|
+
const next = page.pagination.nextPage ?? (!page.pagination.complete && page.pagination.items.length === size ? pageNumber + 1 : null);
|
|
69
|
+
if (next !== null && next <= pageNumber)
|
|
70
|
+
throw new AuditStop('cursor-invalidated');
|
|
71
|
+
return { items: page.items, bytes: page.bytes + verifiedBytes,
|
|
72
|
+
complete: next === null && page.pagination.complete,
|
|
73
|
+
nextCursor: next === null ? null : encodeAuditCursor({ scope, page: next, previous: page.fingerprint, total: page.pagination.totalCount }) };
|
|
74
|
+
},
|
|
75
|
+
async hydrate(issue, options) {
|
|
76
|
+
if (!/^[1-9]\d*$/.test(issue.id))
|
|
77
|
+
throw new AuditStop('invalid-issue-id');
|
|
78
|
+
const response = await transport(`${root}/${issue.id}`, options.signal);
|
|
79
|
+
if (response.body.length > options.maxBytes)
|
|
80
|
+
throw new AuditStop('byte-budget');
|
|
81
|
+
const raw = object(parseForgeJson(response));
|
|
82
|
+
if (raw.updated_at !== issue.updatedAt)
|
|
83
|
+
throw new AuditStop('tracker-changed');
|
|
84
|
+
const body = typeof raw.body === 'string' ? raw.body : '';
|
|
85
|
+
const comments = [];
|
|
86
|
+
let bytes = response.body.length;
|
|
87
|
+
const reader = createForgeReader(provider, async (path, signal) => {
|
|
88
|
+
const response = await transport(path, signal);
|
|
89
|
+
bytes += response.body.length;
|
|
90
|
+
if (bytes > options.maxBytes)
|
|
91
|
+
throw new AuditStop('byte-budget');
|
|
92
|
+
return response;
|
|
93
|
+
}, { pageSize: 25 });
|
|
94
|
+
for (let page = 1;; page++) {
|
|
95
|
+
const result = await reader.listIssueComments(repository, Number(issue.id), page, options.signal);
|
|
96
|
+
comments.push(...result.items.map(c => c.body));
|
|
97
|
+
if (result.complete)
|
|
98
|
+
break;
|
|
99
|
+
if (!result.items.length)
|
|
100
|
+
throw new AuditStop('pagination-incomplete');
|
|
101
|
+
if (result.nextPage !== null && result.nextPage !== page + 1)
|
|
102
|
+
throw new AuditStop('cursor-invalidated');
|
|
103
|
+
if (!result.nextPage && result.items.length < reader.pageSize)
|
|
104
|
+
throw new AuditStop('pagination-incomplete');
|
|
105
|
+
}
|
|
106
|
+
return { body, comments, bytes };
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=issue-pages.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwg/cli",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.12",
|
|
4
4
|
"description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,6 +54,12 @@
|
|
|
54
54
|
"tools/ralph-external/lib/worker-launch-cli.mjs",
|
|
55
55
|
"tools/ralph-external/lib/worker-isolation.mjs",
|
|
56
56
|
"tools/ralph-external/lib/worker-native-sandbox.mjs",
|
|
57
|
+
"tools/ralph-external/lib/worker-isolation-status.mjs",
|
|
58
|
+
"tools/ralph-external/lib/worker-retry-evidence.mjs",
|
|
59
|
+
"tools/ralph-external/lib/worker-termination.mjs",
|
|
60
|
+
"tools/ralph-external/lib/worker-invocation-processes.mjs",
|
|
61
|
+
"tools/ralph-external/lib/worker-invocation-ancestry.mjs",
|
|
62
|
+
"tools/ralph-external/lib/worker-context.mjs",
|
|
57
63
|
"tools/ralph-external/lib/systemd-client.mjs",
|
|
58
64
|
"tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs",
|
|
59
65
|
"tools/ralph-external/lib/launcher-recovery-evidence.mjs",
|