ultra-builder-pro-cli 0.24.0 → 0.24.2
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/.ultra-template/tasks/tasks.json +4 -2
- package/CHANGELOG.md +48 -0
- package/README.md +48 -25
- package/agents/code-reviewer.md +4 -5
- package/agents/debugger.md +8 -8
- package/docs/AGENT-CONTEXT.md +22 -8
- package/docs/ARCHITECTURE.md +30 -9
- package/docs/ARTIFACT-AUTHORITY.md +10 -7
- package/docs/LEGACY-CLI-CROSSWALK.md +1 -1
- package/docs/STATE-DB-ACCESS-POLICY.md +8 -1
- package/docs/WORKFLOW-LIFECYCLE.md +38 -23
- package/hooks/health_check.py +11 -6
- package/mcp-server/lib/artifact-registry.cjs +7 -56
- package/mcp-server/lib/baseline-workflow.cjs +74 -46
- package/mcp-server/lib/change-authority.cjs +26 -0
- package/mcp-server/lib/change-packet.cjs +4 -32
- package/mcp-server/lib/change-workflow.cjs +33 -2724
- package/mcp-server/lib/legacy-change-workflow.cjs +2809 -0
- package/mcp-server/lib/state-ops.cjs +21 -3
- package/mcp-server/lib/task-expander.cjs +6 -1
- package/mcp-server/lib/ultra-facade.cjs +280 -47
- package/mcp-server/server.cjs +1 -1
- package/package.json +1 -1
- package/skills/ultra-change/SKILL.md +2 -1
- package/skills/ultra-deliver/SKILL.md +15 -17
- package/skills/ultra-dev/SKILL.md +3 -1
- package/skills/ultra-doctor/SKILL.md +2 -2
- package/skills/ultra-init/references/brownfield-adoption.md +13 -10
- package/skills/ultra-plan/SKILL.md +10 -8
- package/skills/ultra-research/SKILL.md +8 -4
- package/skills/ultra-review/SKILL.md +6 -4
- package/skills/ultra-test/SKILL.md +6 -4
- package/skills/ultra-think/SKILL.md +3 -2
- package/spec/mcp-tools.yaml +6 -2
package/hooks/health_check.py
CHANGED
|
@@ -73,26 +73,31 @@ def inspect(root: Path) -> dict:
|
|
|
73
73
|
|
|
74
74
|
envelope = context.get("envelope", {}) if context else {}
|
|
75
75
|
diagnostics = envelope.get("diagnostics", {})
|
|
76
|
-
|
|
76
|
+
baseline_blockers = [
|
|
77
77
|
str(item.get("code"))
|
|
78
78
|
for item in [
|
|
79
79
|
*diagnostics.get("needs_attention", []),
|
|
80
|
-
*diagnostics.get("warnings", []),
|
|
81
80
|
*diagnostics.get("hard_conflicts", []),
|
|
82
81
|
]
|
|
83
82
|
if str(item.get("code", "")).startswith("BASELINE_")
|
|
84
83
|
]
|
|
84
|
+
baseline_warnings = [
|
|
85
|
+
str(item.get("code"))
|
|
86
|
+
for item in diagnostics.get("warnings", [])
|
|
87
|
+
if str(item.get("code", "")).startswith("BASELINE_")
|
|
88
|
+
]
|
|
85
89
|
baseline = envelope.get("baseline")
|
|
86
90
|
baseline_is_ready = isinstance(baseline, dict) and baseline.get("status") == "ready"
|
|
87
91
|
baseline_status = (
|
|
88
|
-
"fail" if
|
|
89
|
-
else "warning" if
|
|
92
|
+
"fail" if baseline_blockers and baseline_is_ready
|
|
93
|
+
else "warning" if baseline_blockers or baseline_warnings
|
|
90
94
|
else "pass"
|
|
91
95
|
)
|
|
92
96
|
report["checks"]["baseline"] = {
|
|
93
97
|
"baseline": baseline,
|
|
94
|
-
"blockers":
|
|
95
|
-
"
|
|
98
|
+
"blockers": baseline_blockers,
|
|
99
|
+
"warnings": baseline_warnings,
|
|
100
|
+
"readiness": "blocked" if baseline_blockers else "ready",
|
|
96
101
|
"status": baseline_status,
|
|
97
102
|
}
|
|
98
103
|
if baseline_status == "fail":
|
|
@@ -473,6 +473,13 @@ function invalidateConsumersFromEndpointInTx(db, start, {
|
|
|
473
473
|
const key = endpointKey(endpoint);
|
|
474
474
|
if (seen.has(key) || excluded.has(key) || key === endpointKey(start)) continue;
|
|
475
475
|
seen.add(key);
|
|
476
|
+
if (endpoint.type === 'workflow') {
|
|
477
|
+
// Workflow rows are retained only as pre-v0.24 audit history. Preserve
|
|
478
|
+
// graph traversal for downstream current authorities, but never rewrite
|
|
479
|
+
// the historical workflow row or claim that it was invalidated.
|
|
480
|
+
queue.push(...outgoingEdges(db, endpoint));
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
476
483
|
invalidated.push({ type: endpoint.type, id: endpoint.id });
|
|
477
484
|
if (endpoint.type === 'artifact') {
|
|
478
485
|
const row = db.prepare('SELECT status FROM artifacts WHERE id = ?').get(endpoint.id);
|
|
@@ -503,62 +510,6 @@ function invalidateConsumersFromEndpointInTx(db, start, {
|
|
|
503
510
|
});
|
|
504
511
|
}
|
|
505
512
|
}
|
|
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
513
|
queue.push(...outgoingEdges(db, endpoint));
|
|
563
514
|
}
|
|
564
515
|
return invalidated;
|
|
@@ -660,27 +660,10 @@ function storedReadyInvariantBlockers(baseline) {
|
|
|
660
660
|
if (!Array.isArray(baseline.scope) || baseline.scope.length === 0) {
|
|
661
661
|
blockers.push('BASELINE_SCOPE_MISSING');
|
|
662
662
|
}
|
|
663
|
-
if (Array.isArray(baseline.
|
|
664
|
-
for (const kind of ['discovery', 'product', 'architecture']) {
|
|
665
|
-
if (!baseline.spec_refs.some((item) => item?.kind === kind)) {
|
|
666
|
-
blockers.push(`BASELINE_SPEC_MISSING:${kind}`);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
if (!Array.isArray(baseline.evidence) || baseline.evidence.length === 0) {
|
|
671
|
-
blockers.push('BASELINE_EVIDENCE_MISSING');
|
|
672
|
-
} else if (baseline.mode === 'brownfield'
|
|
673
|
-
&& !baseline.evidence.some((item) => item?.kind === 'source')) {
|
|
674
|
-
blockers.push('BASELINE_SOURCE_EVIDENCE_MISSING');
|
|
675
|
-
}
|
|
676
|
-
if (!Array.isArray(baseline.verification) || baseline.verification.length === 0) {
|
|
677
|
-
blockers.push('BASELINE_VERIFICATION_MISSING');
|
|
678
|
-
} else {
|
|
663
|
+
if (Array.isArray(baseline.verification)) {
|
|
679
664
|
for (const item of baseline.verification) {
|
|
680
665
|
if (!item || typeof item !== 'object') {
|
|
681
666
|
blockers.push('BASELINE_STATE_INVALID:verification');
|
|
682
|
-
} else if (item.status === 'not_run') {
|
|
683
|
-
blockers.push(`BASELINE_VERIFICATION_NOT_RUN:${item.name || 'unknown'}`);
|
|
684
667
|
} else if (item.status === 'known_red') {
|
|
685
668
|
if (!String(item.rationale || '').trim()) {
|
|
686
669
|
blockers.push(`BASELINE_KNOWN_RED_RATIONALE_MISSING:${item.name || 'unknown'}`);
|
|
@@ -688,7 +671,7 @@ function storedReadyInvariantBlockers(baseline) {
|
|
|
688
671
|
if (!baseline.known_red_accepted) {
|
|
689
672
|
blockers.push(`BASELINE_KNOWN_RED_NOT_ACCEPTED:${item.name || 'unknown'}`);
|
|
690
673
|
}
|
|
691
|
-
} else if (item.status
|
|
674
|
+
} else if (!['pass', 'not_run'].includes(item.status)) {
|
|
692
675
|
blockers.push(`BASELINE_VERIFICATION_STATUS_INVALID:${item.name || 'unknown'}`);
|
|
693
676
|
}
|
|
694
677
|
}
|
|
@@ -697,25 +680,54 @@ function storedReadyInvariantBlockers(baseline) {
|
|
|
697
680
|
blockers.push('BASELINE_REVISION_MISSING');
|
|
698
681
|
}
|
|
699
682
|
if (!String(baseline.approved_by || '').trim()) blockers.push('BASELINE_APPROVER_MISSING');
|
|
700
|
-
if (String(baseline.approval_note || '').trim()
|
|
683
|
+
if (!String(baseline.approval_note || '').trim()) {
|
|
701
684
|
blockers.push('BASELINE_APPROVAL_NOTE_MISSING');
|
|
702
685
|
}
|
|
703
686
|
if (!baseline.converged_at || Number.isNaN(Date.parse(baseline.converged_at))) {
|
|
704
687
|
blockers.push('BASELINE_CONVERGENCE_TIMESTAMP_MISSING');
|
|
705
688
|
}
|
|
689
|
+
return [...new Set(blockers)];
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function storedReadySemanticWarnings(baseline) {
|
|
693
|
+
const warnings = [];
|
|
694
|
+
if (Array.isArray(baseline.spec_refs)) {
|
|
695
|
+
for (const kind of ['discovery', 'product', 'architecture']) {
|
|
696
|
+
if (!baseline.spec_refs.some((item) => item?.kind === kind)) {
|
|
697
|
+
warnings.push(`BASELINE_SPEC_MISSING:${kind}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (Array.isArray(baseline.evidence) && baseline.evidence.length === 0) {
|
|
702
|
+
warnings.push('BASELINE_EVIDENCE_MISSING');
|
|
703
|
+
}
|
|
704
|
+
if (baseline.mode === 'brownfield'
|
|
705
|
+
&& Array.isArray(baseline.evidence)
|
|
706
|
+
&& !baseline.evidence.some((item) => item?.kind === 'source')) {
|
|
707
|
+
warnings.push('BASELINE_SOURCE_EVIDENCE_MISSING');
|
|
708
|
+
}
|
|
709
|
+
if (Array.isArray(baseline.verification) && baseline.verification.length === 0) {
|
|
710
|
+
warnings.push('BASELINE_VERIFICATION_MISSING');
|
|
711
|
+
} else if (Array.isArray(baseline.verification)) {
|
|
712
|
+
for (const item of baseline.verification) {
|
|
713
|
+
if (item?.status === 'not_run') {
|
|
714
|
+
warnings.push(`BASELINE_VERIFICATION_NOT_RUN:${item.name || 'unknown'}`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
706
718
|
if (Array.isArray(baseline.unknowns)) {
|
|
707
719
|
for (const item of baseline.unknowns.filter((unknown) => unknown?.blocking === true)) {
|
|
708
|
-
|
|
720
|
+
warnings.push(`BASELINE_UNKNOWN_RECORDED:${item.summary || 'unknown'}`);
|
|
709
721
|
}
|
|
710
722
|
}
|
|
711
723
|
if (Array.isArray(baseline.gaps)) {
|
|
712
724
|
for (const gap of baseline.gaps.filter((item) => (
|
|
713
725
|
item?.status === 'open' && item?.blocking === true
|
|
714
726
|
))) {
|
|
715
|
-
|
|
727
|
+
warnings.push(`BASELINE_GAP_RECORDED:${gap.id || 'unknown'}`);
|
|
716
728
|
}
|
|
717
729
|
}
|
|
718
|
-
return [...new Set(
|
|
730
|
+
return [...new Set(warnings)];
|
|
719
731
|
}
|
|
720
732
|
|
|
721
733
|
function acceptedBaselineResearch(db, baseline) {
|
|
@@ -736,7 +748,7 @@ function convergenceResearchBlockers(db, baseline) {
|
|
|
736
748
|
function storedResearchHealth(db, baseline) {
|
|
737
749
|
const checkpointId = baseline.research_checkpoint_id || baseline.research_run_id;
|
|
738
750
|
if (!checkpointId) {
|
|
739
|
-
return { blockers: [
|
|
751
|
+
return { blockers: [], warnings: ['BASELINE_RESEARCH_PROVENANCE_MISSING'] };
|
|
740
752
|
}
|
|
741
753
|
let checkpoint;
|
|
742
754
|
try {
|
|
@@ -751,10 +763,19 @@ function storedResearchHealth(db, baseline) {
|
|
|
751
763
|
|| checkpoint.stage !== 'research'
|
|
752
764
|
|| checkpoint.scope_type !== 'baseline'
|
|
753
765
|
|| checkpoint.scope_id !== baseline.id
|
|
754
|
-
|| checkpoint.status !== 'accepted'
|
|
755
766
|
|| checkpoints.checkpointDigest(checkpoint) !== checkpoint.digest) {
|
|
756
767
|
return { blockers: ['BASELINE_RESEARCH_RECORD_INVALID'], warnings: [] };
|
|
757
768
|
}
|
|
769
|
+
if (checkpoint.status !== 'accepted') {
|
|
770
|
+
return {
|
|
771
|
+
blockers: [],
|
|
772
|
+
warnings: [
|
|
773
|
+
checkpoint.status === 'superseded'
|
|
774
|
+
? 'BASELINE_RESEARCH_SUPERSEDED'
|
|
775
|
+
: `BASELINE_RESEARCH_NOT_ACCEPTED:${checkpoint.status}`,
|
|
776
|
+
],
|
|
777
|
+
};
|
|
778
|
+
}
|
|
758
779
|
return { blockers: [], warnings: checkpoint.diagnostics || [] };
|
|
759
780
|
}
|
|
760
781
|
|
|
@@ -774,10 +795,6 @@ function convergenceBlockers(db, baseline, input, rootDir) {
|
|
|
774
795
|
if (baseline.repository_branch && snapshot.branch
|
|
775
796
|
&& baseline.repository_branch !== snapshot.branch) blockers.add('BASELINE_BRANCH_STALE');
|
|
776
797
|
if (!Array.isArray(baseline.scope) || baseline.scope.length === 0) blockers.add('BASELINE_SCOPE_MISSING');
|
|
777
|
-
for (const kind of ['discovery', 'product', 'architecture']) {
|
|
778
|
-
if (!baseline.spec_refs.some((ref) => ref.kind === kind)) blockers.add(`BASELINE_SPEC_MISSING:${kind}`);
|
|
779
|
-
}
|
|
780
|
-
for (const blocker of convergenceResearchBlockers(db, baseline)) blockers.add(blocker);
|
|
781
798
|
for (const blocker of storedSpecBlockers(baseline, rootDir)) blockers.add(blocker);
|
|
782
799
|
for (const blocker of storedEvidenceBlockers(baseline, rootDir)) blockers.add(blocker);
|
|
783
800
|
for (const blocker of storedScopeBlockers(baseline, rootDir)) blockers.add(blocker);
|
|
@@ -788,29 +805,23 @@ function convergenceBlockers(db, baseline, input, rootDir) {
|
|
|
788
805
|
if (baseline.worktree_state === 'dirty' && input.accept_dirty_worktree !== true) {
|
|
789
806
|
blockers.add('BASELINE_DIRTY_WORKTREE_NOT_ACCEPTED');
|
|
790
807
|
}
|
|
791
|
-
if (baseline.evidence.length === 0) blockers.add('BASELINE_EVIDENCE_MISSING');
|
|
792
|
-
if (baseline.mode === 'brownfield' && !baseline.evidence.some((item) => item.kind === 'source')) {
|
|
793
|
-
blockers.add('BASELINE_SOURCE_EVIDENCE_MISSING');
|
|
794
|
-
}
|
|
795
|
-
if (baseline.verification.length === 0) blockers.add('BASELINE_VERIFICATION_MISSING');
|
|
796
808
|
for (const item of baseline.verification) {
|
|
797
|
-
if (item.status === 'not_run') blockers.add(`BASELINE_VERIFICATION_NOT_RUN:${item.name}`);
|
|
798
809
|
if (item.status === 'known_red') {
|
|
799
810
|
if (input.accept_known_red !== true) blockers.add(`BASELINE_KNOWN_RED_NOT_ACCEPTED:${item.name}`);
|
|
800
811
|
if (!item.rationale) blockers.add(`BASELINE_KNOWN_RED_RATIONALE_MISSING:${item.name}`);
|
|
801
812
|
}
|
|
802
813
|
}
|
|
803
|
-
for (const item of baseline.unknowns.filter((unknown) => unknown.blocking)) {
|
|
804
|
-
blockers.add(`BASELINE_UNKNOWN_BLOCKING:${item.summary}`);
|
|
805
|
-
}
|
|
806
|
-
for (const gap of baseline.gaps.filter((item) => item.status === 'open' && item.blocking)) {
|
|
807
|
-
blockers.add(`BASELINE_GAP_BLOCKING:${gap.id}`);
|
|
808
|
-
}
|
|
809
814
|
if (!String(input.approved_by || '').trim()) blockers.add('BASELINE_APPROVER_REQUIRED');
|
|
810
|
-
if (String(input.approval_note || '').trim()
|
|
815
|
+
if (!String(input.approval_note || '').trim()) blockers.add('BASELINE_APPROVAL_NOTE_REQUIRED');
|
|
811
816
|
return [...blockers].sort();
|
|
812
817
|
}
|
|
813
818
|
|
|
819
|
+
function convergenceWarnings(db, baseline) {
|
|
820
|
+
const warnings = new Set(storedReadySemanticWarnings(baseline));
|
|
821
|
+
for (const warning of convergenceResearchBlockers(db, baseline)) warnings.add(warning);
|
|
822
|
+
return [...warnings].sort();
|
|
823
|
+
}
|
|
824
|
+
|
|
814
825
|
function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = true } = {}) {
|
|
815
826
|
const current = readBaseline(db, input.id);
|
|
816
827
|
if (!current) throw new BaselineWorkflowError('BASELINE_NOT_FOUND', `baseline ${input.id} not found`);
|
|
@@ -818,6 +829,7 @@ function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent =
|
|
|
818
829
|
throw new BaselineWorkflowError('BASELINE_NOT_CONVERGEABLE', `baseline ${current.id} is ${current.status}`);
|
|
819
830
|
}
|
|
820
831
|
const blockers = convergenceBlockers(db, current, input, rootDir);
|
|
832
|
+
const warnings = convergenceWarnings(db, current);
|
|
821
833
|
const ts = nowIso();
|
|
822
834
|
if (blockers.length > 0) {
|
|
823
835
|
ops.tx(db, () => {
|
|
@@ -829,7 +841,13 @@ function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent =
|
|
|
829
841
|
});
|
|
830
842
|
}
|
|
831
843
|
});
|
|
832
|
-
return {
|
|
844
|
+
return {
|
|
845
|
+
ready: false,
|
|
846
|
+
status: 'blocked',
|
|
847
|
+
blockers,
|
|
848
|
+
warnings,
|
|
849
|
+
baseline: readBaseline(db, current.id),
|
|
850
|
+
};
|
|
833
851
|
}
|
|
834
852
|
const research = acceptedBaselineResearch(db, current);
|
|
835
853
|
ops.tx(db, () => {
|
|
@@ -841,7 +859,7 @@ function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent =
|
|
|
841
859
|
).run(
|
|
842
860
|
String(input.approved_by).trim(), String(input.approval_note).trim(),
|
|
843
861
|
input.accept_dirty_worktree === true ? 1 : 0,
|
|
844
|
-
input.accept_known_red === true ? 1 : 0, research
|
|
862
|
+
input.accept_known_red === true ? 1 : 0, research?.id || null, ts, ts, current.id,
|
|
845
863
|
);
|
|
846
864
|
if (emitEvent) {
|
|
847
865
|
ops.appendEventInTx(db, {
|
|
@@ -849,7 +867,13 @@ function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent =
|
|
|
849
867
|
});
|
|
850
868
|
}
|
|
851
869
|
});
|
|
852
|
-
return {
|
|
870
|
+
return {
|
|
871
|
+
ready: true,
|
|
872
|
+
status: 'ready',
|
|
873
|
+
blockers: [],
|
|
874
|
+
warnings,
|
|
875
|
+
baseline: readBaseline(db, current.id),
|
|
876
|
+
};
|
|
853
877
|
}
|
|
854
878
|
|
|
855
879
|
function inspectBaseline(db, { rootDir = process.cwd(), id } = {}) {
|
|
@@ -880,6 +904,10 @@ function inspectBaseline(db, { rootDir = process.cwd(), id } = {}) {
|
|
|
880
904
|
...(Array.isArray(baseline.evidence) ? storedEvidenceBlockers(baseline, rootDir) : []),
|
|
881
905
|
...research.blockers,
|
|
882
906
|
];
|
|
907
|
+
const warnings = [
|
|
908
|
+
...storedReadySemanticWarnings(baseline),
|
|
909
|
+
...research.warnings,
|
|
910
|
+
];
|
|
883
911
|
const snapshot = gitWorktreeSnapshot(
|
|
884
912
|
rootDir, Array.isArray(baseline.scope) && baseline.scope.length > 0 ? baseline.scope : ['.'],
|
|
885
913
|
);
|
|
@@ -909,7 +937,7 @@ function inspectBaseline(db, { rootDir = process.cwd(), id } = {}) {
|
|
|
909
937
|
}
|
|
910
938
|
return {
|
|
911
939
|
status: blockers.length === 0 ? 'pass' : 'fail', blockers: [...new Set(blockers)].sort(),
|
|
912
|
-
warnings:
|
|
940
|
+
warnings: [...new Set(warnings)], baseline,
|
|
913
941
|
};
|
|
914
942
|
}
|
|
915
943
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
function parseJson(value, fallback = {}) {
|
|
6
|
+
if (typeof value !== 'string' || !value.trim()) return fallback;
|
|
7
|
+
try { return JSON.parse(value); } catch { return fallback; }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function changeStateDigest(change) {
|
|
11
|
+
if (!change) return null;
|
|
12
|
+
const payload = {
|
|
13
|
+
id: change.id,
|
|
14
|
+
kind: change.kind,
|
|
15
|
+
intent: change.intent,
|
|
16
|
+
docs_impact: change.docs_impact || parseJson(change.docs_impact_json, {}),
|
|
17
|
+
provider_refs: change.provider_refs || parseJson(change.provider_refs_json, {}),
|
|
18
|
+
contract: change.contract || parseJson(change.contract_json, {}),
|
|
19
|
+
classification: change.classification || parseJson(change.classification_json, {}),
|
|
20
|
+
research_disposition: change.research_disposition
|
|
21
|
+
|| parseJson(change.research_disposition_json, {}),
|
|
22
|
+
};
|
|
23
|
+
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { changeStateDigest };
|
|
@@ -77,25 +77,8 @@ function requireInside(rootDir, relative, prefix, field) {
|
|
|
77
77
|
return resolved;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
`SELECT id FROM workflow_runs
|
|
83
|
-
WHERE change_id = ?
|
|
84
|
-
ORDER BY CASE
|
|
85
|
-
WHEN status IN ('active', 'blocked', 'ready') THEN 0
|
|
86
|
-
WHEN status = 'completed' THEN 1
|
|
87
|
-
ELSE 2 END,
|
|
88
|
-
CASE kind WHEN 'deliver' THEN 0 WHEN 'plan' THEN 1 WHEN 'research' THEN 2 ELSE 3 END,
|
|
89
|
-
updated_at DESC, rowid DESC
|
|
90
|
-
LIMIT 1`,
|
|
91
|
-
).get(changeId);
|
|
92
|
-
if (!row) {
|
|
93
|
-
throw new ChangePacketError(
|
|
94
|
-
'ARTIFACT_CONSUMER_MISSING',
|
|
95
|
-
`change ${changeId} has no workflow consumer`,
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
return { type: 'workflow', id: row.id, relation: 'consumed_by' };
|
|
80
|
+
function deliveryConsumer() {
|
|
81
|
+
return { type: 'external', id: 'ultra-deliver', relation: 'consumed_by' };
|
|
99
82
|
}
|
|
100
83
|
|
|
101
84
|
function readCurrentArtifact(db, changeId, kind) {
|
|
@@ -118,17 +101,6 @@ function exactBaselineAnchor(db, change, input) {
|
|
|
118
101
|
'typed Change delta requires one approved ready baseline',
|
|
119
102
|
);
|
|
120
103
|
}
|
|
121
|
-
const changeRun = db.prepare(
|
|
122
|
-
`SELECT baseline_id FROM workflow_runs
|
|
123
|
-
WHERE kind = 'change' AND change_id = ?
|
|
124
|
-
ORDER BY started_at DESC, rowid DESC LIMIT 1`,
|
|
125
|
-
).get(change.id);
|
|
126
|
-
if (!changeRun || changeRun.baseline_id !== baseline.id) {
|
|
127
|
-
throw new ChangePacketError(
|
|
128
|
-
'CHANGE_BASELINE_AUTHORITY_MISMATCH',
|
|
129
|
-
`change ${change.id} is not bound to baseline ${baseline.id}`,
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
104
|
const anchor = input.baseline_anchor;
|
|
133
105
|
if (!anchor || anchor.baseline_id !== baseline.id
|
|
134
106
|
|| (anchor.repository_revision || null) !== (baseline.repository_revision || null)) {
|
|
@@ -371,7 +343,7 @@ function recordDelta(db, change, input, { rootDir = process.cwd() } = {}) {
|
|
|
371
343
|
let rollbackProgress = null;
|
|
372
344
|
try {
|
|
373
345
|
const result = ops.tx(db, () => {
|
|
374
|
-
const consumer =
|
|
346
|
+
const consumer = deliveryConsumer();
|
|
375
347
|
const payloads = delta.mutations.map((mutation) => (
|
|
376
348
|
artifacts.recordArtifactInTx(db, {
|
|
377
349
|
id: `change-${change.id}-delta-${mutation.id}`,
|
|
@@ -578,7 +550,7 @@ function recordDocumentationReconciliation(
|
|
|
578
550
|
let rollbackProgress = null;
|
|
579
551
|
try {
|
|
580
552
|
return ops.tx(db, () => {
|
|
581
|
-
const fallbackConsumer =
|
|
553
|
+
const fallbackConsumer = deliveryConsumer();
|
|
582
554
|
const payloads = normalized.reconciliation.documents.map((document, index) => (
|
|
583
555
|
artifacts.recordArtifactInTx(db, {
|
|
584
556
|
id: `change-${change.id}-documentation-${index + 1}`,
|