ultra-builder-pro-cli 0.24.1 → 0.25.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/CHANGELOG.md +63 -0
- package/README.md +10 -0
- package/adapters/claude.js +104 -0
- package/adapters/codex.js +363 -40
- package/adapters/grok.js +1254 -58
- package/adapters/kimi.js +682 -13
- package/adapters/opencode.js +132 -11
- package/agents/code-reviewer.md +4 -5
- package/agents/debugger.md +8 -8
- package/docs/AGENT-CONTEXT.md +30 -4
- package/docs/ARCHITECTURE.md +24 -5
- package/docs/ARTIFACT-AUTHORITY.md +13 -0
- package/docs/COMMIT-HASH-BACKFILL.md +25 -18
- package/docs/RUNTIME-COMPAT-MATRIX.md +18 -2
- package/docs/STATE-DB-ACCESS-POLICY.md +8 -1
- package/mcp-server/lib/artifact-registry.cjs +188 -107
- package/mcp-server/lib/baseline-workflow.cjs +160 -23
- package/mcp-server/lib/change-packet.cjs +4 -32
- package/mcp-server/lib/change-workflow.cjs +36 -2755
- package/mcp-server/lib/context-envelope.cjs +124 -51
- package/mcp-server/lib/decision-records.cjs +164 -78
- package/mcp-server/lib/kernel-change-workflow.cjs +2029 -0
- package/mcp-server/lib/legacy-change-workflow.cjs +3032 -0
- package/mcp-server/lib/managed-file-write.cjs +56 -0
- package/mcp-server/lib/prd-parser.cjs +2 -2
- package/mcp-server/lib/stage-checkpoints.cjs +163 -9
- package/mcp-server/lib/state-db.cjs +186 -8
- package/mcp-server/lib/state-ops.cjs +214 -31
- package/mcp-server/lib/task-contract.cjs +404 -0
- package/mcp-server/lib/task-expander.cjs +8 -3
- package/mcp-server/lib/task-ledger.cjs +175 -62
- package/mcp-server/lib/ultra-facade.cjs +1895 -172
- package/mcp-server/lib/worker-packet.cjs +2 -2
- package/mcp-server/server.cjs +24 -5
- package/orchestrator/parallel-orchestrator.cjs +114 -27
- package/orchestrator/session-runner.cjs +25 -136
- package/package.json +1 -1
- package/skills/ultra-change/SKILL.md +6 -1
- package/skills/ultra-dev/SKILL.md +9 -4
- package/skills/ultra-plan/SKILL.md +13 -4
- package/skills/ultra-plan/references/semantic-preflight.md +6 -4
- package/spec/mcp-tools.yaml +114 -9
- package/spec/schemas/context-file.v4.5.schema.json +2 -2
- package/spec/schemas/state-db.sql +10 -6
- package/spec/schemas/task-ledger.v2.schema.json +23 -8
- package/spec/schemas/tasks.v4.5.schema.json +34 -12
|
@@ -17,6 +17,12 @@ const ENDPOINT_TYPES = new Set([
|
|
|
17
17
|
]);
|
|
18
18
|
const OWNER_TYPES = new Set(['project', 'baseline', 'change', 'task', 'workflow']);
|
|
19
19
|
const STATUSES = new Set(['current', 'stale', 'terminal', 'archived']);
|
|
20
|
+
const ARTIFACT_INPUT_FIELDS = new Set([
|
|
21
|
+
'id', 'owner_type', 'owner_id', 'change_id', 'task_id', 'kind', 'path',
|
|
22
|
+
'content_digest', 'expected_before_digest', 'source_refs', 'consumer_refs',
|
|
23
|
+
'provenance', 'metadata', 'status',
|
|
24
|
+
]);
|
|
25
|
+
const DIGEST = /^[0-9a-f]{64}$/;
|
|
20
26
|
const OWNER_TABLES = Object.freeze({
|
|
21
27
|
baseline: 'baselines',
|
|
22
28
|
change: 'changes',
|
|
@@ -48,6 +54,70 @@ class ArtifactRegistryError extends Error {
|
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
56
|
|
|
57
|
+
function artifactText(value, field, { optional = false, nullable = false } = {}) {
|
|
58
|
+
if (value === undefined && optional) return undefined;
|
|
59
|
+
if (value === null && nullable) return null;
|
|
60
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
61
|
+
throw new ArtifactRegistryError(
|
|
62
|
+
'VALIDATION_ERROR',
|
|
63
|
+
`${field} must be a non-empty string${nullable ? ' or null' : ''}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return value.trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function artifactDigest(value, field, { nullable = false } = {}) {
|
|
70
|
+
if (value === undefined) return undefined;
|
|
71
|
+
if (value === null && nullable) return null;
|
|
72
|
+
if (typeof value !== 'string' || !DIGEST.test(value)) {
|
|
73
|
+
throw new ArtifactRegistryError(
|
|
74
|
+
'VALIDATION_ERROR',
|
|
75
|
+
`${field} must be a lowercase SHA-256 digest${nullable ? ' or null' : ''}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function assertJsonValue(value, field, seen = new Set()) {
|
|
82
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return;
|
|
83
|
+
if (typeof value === 'number') {
|
|
84
|
+
if (Number.isFinite(value)) return;
|
|
85
|
+
throw new ArtifactRegistryError('VALIDATION_ERROR', `${field} must contain valid JSON values`);
|
|
86
|
+
}
|
|
87
|
+
if (typeof value !== 'object') {
|
|
88
|
+
throw new ArtifactRegistryError('VALIDATION_ERROR', `${field} must contain valid JSON values`);
|
|
89
|
+
}
|
|
90
|
+
if (seen.has(value)) {
|
|
91
|
+
throw new ArtifactRegistryError('VALIDATION_ERROR', `${field} must not contain circular references`);
|
|
92
|
+
}
|
|
93
|
+
if (!Array.isArray(value)) {
|
|
94
|
+
const prototype = Object.getPrototypeOf(value);
|
|
95
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
96
|
+
throw new ArtifactRegistryError(
|
|
97
|
+
'VALIDATION_ERROR', `${field} must contain only JSON objects`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
seen.add(value);
|
|
102
|
+
if (Array.isArray(value)) {
|
|
103
|
+
value.forEach((item, index) => assertJsonValue(item, `${field}[${index}]`, seen));
|
|
104
|
+
} else {
|
|
105
|
+
Object.entries(value).forEach(([key, item]) => (
|
|
106
|
+
assertJsonValue(item, `${field}.${key}`, seen)
|
|
107
|
+
));
|
|
108
|
+
}
|
|
109
|
+
seen.delete(value);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeJsonObject(value, field) {
|
|
113
|
+
if (value === undefined) return {};
|
|
114
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
115
|
+
throw new ArtifactRegistryError('VALIDATION_ERROR', `${field} must be a JSON object`);
|
|
116
|
+
}
|
|
117
|
+
assertJsonValue(value, field);
|
|
118
|
+
return JSON.parse(JSON.stringify(value));
|
|
119
|
+
}
|
|
120
|
+
|
|
51
121
|
function parseJson(value, fallback) {
|
|
52
122
|
if (value == null || value === '') return fallback;
|
|
53
123
|
try { return JSON.parse(value); } catch { return fallback; }
|
|
@@ -302,6 +372,13 @@ function normalizeEndpoint(endpoint, field) {
|
|
|
302
372
|
if (!endpoint || typeof endpoint !== 'object' || Array.isArray(endpoint)) {
|
|
303
373
|
throw new ArtifactRegistryError('VALIDATION_ERROR', `${field} must be an object`);
|
|
304
374
|
}
|
|
375
|
+
const unknown = Object.keys(endpoint)
|
|
376
|
+
.filter((key) => !['type', 'id', 'relation'].includes(key));
|
|
377
|
+
if (unknown.length > 0) {
|
|
378
|
+
throw new ArtifactRegistryError(
|
|
379
|
+
'VALIDATION_ERROR', `${field}.${unknown[0]} is not allowed`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
305
382
|
const type = typeof endpoint.type === 'string' ? endpoint.type.trim() : '';
|
|
306
383
|
const id = typeof endpoint.id === 'string' ? endpoint.id.trim() : '';
|
|
307
384
|
const relation = typeof endpoint.relation === 'string' ? endpoint.relation.trim() : '';
|
|
@@ -313,6 +390,59 @@ function normalizeEndpoint(endpoint, field) {
|
|
|
313
390
|
return { type, id, relation };
|
|
314
391
|
}
|
|
315
392
|
|
|
393
|
+
function normalizeArtifactInput(input) {
|
|
394
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
395
|
+
throw new ArtifactRegistryError('VALIDATION_ERROR', 'artifact input is required');
|
|
396
|
+
}
|
|
397
|
+
const unknown = Object.keys(input).filter((field) => !ARTIFACT_INPUT_FIELDS.has(field));
|
|
398
|
+
if (unknown.length > 0) {
|
|
399
|
+
throw new ArtifactRegistryError(
|
|
400
|
+
'VALIDATION_ERROR', `artifact.${unknown[0]} is not allowed`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
const status = input.status === undefined
|
|
404
|
+
? 'current'
|
|
405
|
+
: artifactText(input.status, 'artifact.status');
|
|
406
|
+
if (!STATUSES.has(status)) {
|
|
407
|
+
throw new ArtifactRegistryError('VALIDATION_ERROR', `invalid artifact status: ${status}`);
|
|
408
|
+
}
|
|
409
|
+
const normalizeRefs = (value, field) => {
|
|
410
|
+
if (value === undefined) return [];
|
|
411
|
+
if (!Array.isArray(value)) {
|
|
412
|
+
throw new ArtifactRegistryError('VALIDATION_ERROR', `${field} must be an array`);
|
|
413
|
+
}
|
|
414
|
+
return value.map((item, index) => normalizeEndpoint(item, `${field}[${index}]`));
|
|
415
|
+
};
|
|
416
|
+
return {
|
|
417
|
+
id: artifactText(input.id, 'artifact.id', { optional: true }),
|
|
418
|
+
ownerType: artifactText(input.owner_type, 'artifact.owner_type'),
|
|
419
|
+
ownerId: artifactText(input.owner_id, 'artifact.owner_id'),
|
|
420
|
+
changeId: artifactText(
|
|
421
|
+
input.change_id,
|
|
422
|
+
'artifact.change_id',
|
|
423
|
+
{ optional: true, nullable: true },
|
|
424
|
+
),
|
|
425
|
+
taskId: artifactText(
|
|
426
|
+
input.task_id,
|
|
427
|
+
'artifact.task_id',
|
|
428
|
+
{ optional: true, nullable: true },
|
|
429
|
+
),
|
|
430
|
+
kind: artifactText(input.kind, 'artifact.kind'),
|
|
431
|
+
path: artifactText(input.path, 'artifact.path'),
|
|
432
|
+
status,
|
|
433
|
+
contentDigest: artifactDigest(input.content_digest, 'artifact.content_digest'),
|
|
434
|
+
expectedBeforeDigest: artifactDigest(
|
|
435
|
+
input.expected_before_digest,
|
|
436
|
+
'artifact.expected_before_digest',
|
|
437
|
+
{ nullable: true },
|
|
438
|
+
),
|
|
439
|
+
sourceRefs: normalizeRefs(input.source_refs, 'artifact.source_refs'),
|
|
440
|
+
consumerRefs: normalizeRefs(input.consumer_refs, 'artifact.consumer_refs'),
|
|
441
|
+
provenance: normalizeJsonObject(input.provenance, 'artifact.provenance'),
|
|
442
|
+
metadata: normalizeJsonObject(input.metadata, 'artifact.metadata'),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
316
446
|
function assertEndpoint(db, endpoint, field) {
|
|
317
447
|
const normalized = normalizeEndpoint(endpoint, field);
|
|
318
448
|
const { type, id } = normalized;
|
|
@@ -339,6 +469,34 @@ function assertOwner(db, ownerType, ownerId) {
|
|
|
339
469
|
}
|
|
340
470
|
}
|
|
341
471
|
|
|
472
|
+
function assertArtifactOwner(db, ownerType, ownerId) {
|
|
473
|
+
assertOwner(db, ownerType, ownerId);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function assertDeclaredOwnerRefs(db, normalized) {
|
|
477
|
+
const expectedTaskId = normalized.ownerType === 'task' ? normalized.ownerId : null;
|
|
478
|
+
const expectedChangeId = normalized.ownerType === 'change'
|
|
479
|
+
? normalized.ownerId
|
|
480
|
+
: normalized.ownerType === 'task'
|
|
481
|
+
? db.prepare('SELECT change_id FROM tasks WHERE id = ?').get(normalized.ownerId)?.change_id || null
|
|
482
|
+
: normalized.ownerType === 'workflow'
|
|
483
|
+
? db.prepare('SELECT change_id FROM workflow_runs WHERE id = ?').get(normalized.ownerId)?.change_id || null
|
|
484
|
+
: null;
|
|
485
|
+
for (const [field, supplied, expected] of [
|
|
486
|
+
['change_id', normalized.changeId, expectedChangeId],
|
|
487
|
+
['task_id', normalized.taskId, expectedTaskId],
|
|
488
|
+
]) {
|
|
489
|
+
if (supplied !== undefined && supplied !== expected) {
|
|
490
|
+
throw new ArtifactRegistryError(
|
|
491
|
+
'ARTIFACT_AUTHORITY_CONFLICT',
|
|
492
|
+
`artifact ${field} does not match its owner authority`,
|
|
493
|
+
{ field, supplied, expected },
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return { changeId: expectedChangeId, taskId: expectedTaskId };
|
|
498
|
+
}
|
|
499
|
+
|
|
342
500
|
function edgesForArtifact(db, artifactId) {
|
|
343
501
|
const sources = db.prepare(
|
|
344
502
|
`SELECT source_type AS type, source_id AS id, relation
|
|
@@ -473,6 +631,13 @@ function invalidateConsumersFromEndpointInTx(db, start, {
|
|
|
473
631
|
const key = endpointKey(endpoint);
|
|
474
632
|
if (seen.has(key) || excluded.has(key) || key === endpointKey(start)) continue;
|
|
475
633
|
seen.add(key);
|
|
634
|
+
if (endpoint.type === 'workflow') {
|
|
635
|
+
// Workflow rows are retained only as pre-v0.24 audit history. Preserve
|
|
636
|
+
// graph traversal for downstream current authorities, but never rewrite
|
|
637
|
+
// the historical workflow row or claim that it was invalidated.
|
|
638
|
+
queue.push(...outgoingEdges(db, endpoint));
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
476
641
|
invalidated.push({ type: endpoint.type, id: endpoint.id });
|
|
477
642
|
if (endpoint.type === 'artifact') {
|
|
478
643
|
const row = db.prepare('SELECT status FROM artifacts WHERE id = ?').get(endpoint.id);
|
|
@@ -503,62 +668,6 @@ function invalidateConsumersFromEndpointInTx(db, start, {
|
|
|
503
668
|
});
|
|
504
669
|
}
|
|
505
670
|
}
|
|
506
|
-
if (endpoint.type === 'workflow') {
|
|
507
|
-
const row = db.prepare(
|
|
508
|
-
'SELECT status, metadata_json, change_id, task_id FROM workflow_runs WHERE id = ?',
|
|
509
|
-
).get(endpoint.id);
|
|
510
|
-
if (row) {
|
|
511
|
-
const metadata = parseJson(row.metadata_json, {});
|
|
512
|
-
if (['active', 'blocked', 'ready'].includes(row.status)) {
|
|
513
|
-
delete metadata.authority_invalidation;
|
|
514
|
-
metadata.draft_dirty = true;
|
|
515
|
-
db.prepare(
|
|
516
|
-
`UPDATE workflow_runs
|
|
517
|
-
SET status = CASE WHEN status = 'ready' THEN 'active' ELSE status END,
|
|
518
|
-
metadata_json = ?,
|
|
519
|
-
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
520
|
-
WHERE id = ?`,
|
|
521
|
-
).run(JSON.stringify(metadata), endpoint.id);
|
|
522
|
-
ops.appendEventInTx(db, {
|
|
523
|
-
type: 'workflow_draft_changed',
|
|
524
|
-
change_id: row.change_id,
|
|
525
|
-
task_id: row.task_id,
|
|
526
|
-
payload: {
|
|
527
|
-
workflow_id: endpoint.id,
|
|
528
|
-
source_type: start.type,
|
|
529
|
-
source_id: start.id,
|
|
530
|
-
reason,
|
|
531
|
-
},
|
|
532
|
-
});
|
|
533
|
-
queue.push(...outgoingEdges(db, endpoint));
|
|
534
|
-
continue;
|
|
535
|
-
}
|
|
536
|
-
if (metadata.authority_invalidation?.invalidated !== true) {
|
|
537
|
-
metadata.authority_invalidation = {
|
|
538
|
-
invalidated: true,
|
|
539
|
-
source_type: start.type,
|
|
540
|
-
source_id: start.id,
|
|
541
|
-
reason,
|
|
542
|
-
invalidated_at: new Date().toISOString(),
|
|
543
|
-
};
|
|
544
|
-
db.prepare(
|
|
545
|
-
`UPDATE workflow_runs SET metadata_json = ?,
|
|
546
|
-
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`,
|
|
547
|
-
).run(JSON.stringify(metadata), endpoint.id);
|
|
548
|
-
ops.appendEventInTx(db, {
|
|
549
|
-
type: 'workflow_invalidated',
|
|
550
|
-
change_id: row.change_id,
|
|
551
|
-
task_id: row.task_id,
|
|
552
|
-
payload: {
|
|
553
|
-
workflow_id: endpoint.id,
|
|
554
|
-
source_type: start.type,
|
|
555
|
-
source_id: start.id,
|
|
556
|
-
reason,
|
|
557
|
-
},
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
671
|
queue.push(...outgoingEdges(db, endpoint));
|
|
563
672
|
}
|
|
564
673
|
return invalidated;
|
|
@@ -576,28 +685,20 @@ function invalidateDownstreamInTx(db, artifactId, priorConsumers = []) {
|
|
|
576
685
|
}
|
|
577
686
|
|
|
578
687
|
function preflightArtifactPublication(db, input, { rootDir = process.cwd() } = {}) {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const ownerId = typeof input.owner_id === 'string' ? input.owner_id.trim() : '';
|
|
584
|
-
const kind = typeof input.kind === 'string' ? input.kind.trim() : '';
|
|
585
|
-
const status = input.status || 'current';
|
|
586
|
-
if (!kind || !STATUSES.has(status)) {
|
|
587
|
-
throw new ArtifactRegistryError(
|
|
588
|
-
'VALIDATION_ERROR', 'artifact kind and valid status are required',
|
|
589
|
-
);
|
|
590
|
-
}
|
|
688
|
+
const normalized = normalizeArtifactInput(input);
|
|
689
|
+
const {
|
|
690
|
+
ownerType, ownerId, kind, status,
|
|
691
|
+
} = normalized;
|
|
591
692
|
assertOwner(db, ownerType, ownerId);
|
|
592
|
-
|
|
693
|
+
assertDeclaredOwnerRefs(db, normalized);
|
|
694
|
+
const resolved = resolveArtifactFile(rootDir, normalized.path);
|
|
593
695
|
if (isExemptArtifactPath(resolved.relative) || isGeneratedProjectionPath(resolved.relative)) {
|
|
594
696
|
throw new ArtifactRegistryError(
|
|
595
697
|
'ARTIFACT_PATH_EXEMPT',
|
|
596
698
|
`runtime, scratch, and generated projection paths are not registry authority: ${resolved.relative}`,
|
|
597
699
|
);
|
|
598
700
|
}
|
|
599
|
-
const explicitId =
|
|
600
|
-
? input.id.trim() : null;
|
|
701
|
+
const explicitId = normalized.id || null;
|
|
601
702
|
const existingById = explicitId
|
|
602
703
|
? db.prepare('SELECT * FROM artifacts WHERE id = ?').get(explicitId)
|
|
603
704
|
: null;
|
|
@@ -674,18 +775,14 @@ function preflightArtifactPublication(db, input, { rootDir = process.cwd() } = {
|
|
|
674
775
|
}
|
|
675
776
|
|
|
676
777
|
function recordArtifactInTx(db, input, { rootDir = process.cwd() } = {}) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const kind = typeof input.kind === 'string' ? input.kind.trim() : '';
|
|
683
|
-
const status = input.status || 'current';
|
|
684
|
-
if (!kind || !STATUSES.has(status)) {
|
|
685
|
-
throw new ArtifactRegistryError('VALIDATION_ERROR', 'artifact kind and valid status are required');
|
|
686
|
-
}
|
|
778
|
+
const normalized = normalizeArtifactInput(input);
|
|
779
|
+
const {
|
|
780
|
+
ownerType, ownerId, kind, status,
|
|
781
|
+
sourceRefs, consumerRefs, provenance, metadata,
|
|
782
|
+
} = normalized;
|
|
687
783
|
assertOwner(db, ownerType, ownerId);
|
|
688
|
-
const
|
|
784
|
+
const ownerRefs = assertDeclaredOwnerRefs(db, normalized);
|
|
785
|
+
const resolved = resolveArtifactFile(rootDir, normalized.path);
|
|
689
786
|
if (isExemptArtifactPath(resolved.relative) || isGeneratedProjectionPath(resolved.relative)) {
|
|
690
787
|
throw new ArtifactRegistryError(
|
|
691
788
|
'ARTIFACT_PATH_EXEMPT',
|
|
@@ -694,18 +791,7 @@ function recordArtifactInTx(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
694
791
|
}
|
|
695
792
|
const stableRead = openStableArtifactRead(resolved);
|
|
696
793
|
try {
|
|
697
|
-
const
|
|
698
|
-
? input.source_refs.map((item, index) => normalizeEndpoint(item, `source_refs[${index}]`))
|
|
699
|
-
: [];
|
|
700
|
-
const consumerRefs = Array.isArray(input.consumer_refs)
|
|
701
|
-
? input.consumer_refs.map((item, index) => normalizeEndpoint(item, `consumer_refs[${index}]`))
|
|
702
|
-
: [];
|
|
703
|
-
const provenance = input.provenance && typeof input.provenance === 'object'
|
|
704
|
-
&& !Array.isArray(input.provenance) ? input.provenance : {};
|
|
705
|
-
const metadata = input.metadata && typeof input.metadata === 'object'
|
|
706
|
-
&& !Array.isArray(input.metadata) ? input.metadata : {};
|
|
707
|
-
const explicitId = typeof input.id === 'string' && input.id.trim() !== ''
|
|
708
|
-
? input.id.trim() : null;
|
|
794
|
+
const explicitId = normalized.id || null;
|
|
709
795
|
const existingById = explicitId
|
|
710
796
|
? db.prepare('SELECT * FROM artifacts WHERE id = ?').get(explicitId)
|
|
711
797
|
: null;
|
|
@@ -759,31 +845,25 @@ function recordArtifactInTx(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
759
845
|
});
|
|
760
846
|
}
|
|
761
847
|
const beforeDigest = existing?.digest || existing?.content_hash || null;
|
|
762
|
-
if (
|
|
763
|
-
&&
|
|
848
|
+
if (normalized.expectedBeforeDigest !== undefined
|
|
849
|
+
&& normalized.expectedBeforeDigest !== beforeDigest) {
|
|
764
850
|
throw new ArtifactRegistryError(
|
|
765
851
|
'ARTIFACT_DIGEST_CONFLICT',
|
|
766
852
|
`artifact ${artifactId} changed since it was read`,
|
|
767
|
-
{ expected:
|
|
853
|
+
{ expected: normalized.expectedBeforeDigest, actual: beforeDigest },
|
|
768
854
|
);
|
|
769
855
|
}
|
|
770
856
|
const afterDigest = stableRead.digest;
|
|
771
|
-
if (
|
|
857
|
+
if (normalized.contentDigest !== undefined && normalized.contentDigest !== afterDigest) {
|
|
772
858
|
throw new ArtifactRegistryError(
|
|
773
859
|
'ARTIFACT_DIGEST_CONFLICT',
|
|
774
860
|
`artifact bytes do not match the supplied digest: ${resolved.relative}`,
|
|
775
|
-
{ expected:
|
|
861
|
+
{ expected: normalized.contentDigest, actual: afterDigest },
|
|
776
862
|
);
|
|
777
863
|
}
|
|
778
864
|
const changed = beforeDigest !== afterDigest;
|
|
779
865
|
const priorConsumers = existing ? edgesForArtifact(db, artifactId).consumers : [];
|
|
780
|
-
const ownerChangeId =
|
|
781
|
-
? ownerId
|
|
782
|
-
: ownerType === 'task'
|
|
783
|
-
? db.prepare('SELECT change_id FROM tasks WHERE id = ?').get(ownerId)?.change_id || null
|
|
784
|
-
: ownerType === 'workflow'
|
|
785
|
-
? db.prepare('SELECT change_id FROM workflow_runs WHERE id = ?').get(ownerId)?.change_id || null
|
|
786
|
-
: null;
|
|
866
|
+
const ownerChangeId = ownerRefs.changeId;
|
|
787
867
|
const now = new Date().toISOString();
|
|
788
868
|
let invalidated = [];
|
|
789
869
|
db.prepare(
|
|
@@ -803,7 +883,7 @@ function recordArtifactInTx(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
803
883
|
).run(
|
|
804
884
|
artifactId, ownerType, ownerId,
|
|
805
885
|
ownerChangeId,
|
|
806
|
-
|
|
886
|
+
ownerRefs.taskId,
|
|
807
887
|
kind, resolved.relative, afterDigest, afterDigest, beforeDigest, afterDigest,
|
|
808
888
|
JSON.stringify(provenance), JSON.stringify(metadata), status,
|
|
809
889
|
existing?.created_at || now, now,
|
|
@@ -1134,6 +1214,7 @@ module.exports = {
|
|
|
1134
1214
|
ENDPOINT_TYPES,
|
|
1135
1215
|
OWNER_TYPES,
|
|
1136
1216
|
STATUSES,
|
|
1217
|
+
assertArtifactOwner,
|
|
1137
1218
|
digestFile,
|
|
1138
1219
|
getArtifact,
|
|
1139
1220
|
inspectArtifactHealth,
|