ultra-builder-pro-cli 0.24.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ultra-template/tasks/tasks.json +4 -2
- package/CHANGELOG.md +27 -0
- package/README.md +38 -25
- package/docs/AGENT-CONTEXT.md +6 -4
- package/docs/ARCHITECTURE.md +6 -4
- package/docs/ARTIFACT-AUTHORITY.md +10 -7
- package/docs/LEGACY-CLI-CROSSWALK.md +1 -1
- package/docs/WORKFLOW-LIFECYCLE.md +38 -23
- package/hooks/health_check.py +11 -6
- package/mcp-server/lib/baseline-workflow.cjs +74 -46
- package/mcp-server/lib/change-authority.cjs +26 -0
- package/mcp-server/lib/change-workflow.cjs +69 -36
- package/mcp-server/lib/state-ops.cjs +21 -3
- package/mcp-server/lib/ultra-facade.cjs +159 -32
- 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
|
@@ -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 };
|
|
@@ -9,6 +9,7 @@ const Ajv = require('ajv/dist/2020');
|
|
|
9
9
|
const ops = require('./state-ops.cjs');
|
|
10
10
|
const artifacts = require('./artifact-registry.cjs');
|
|
11
11
|
const canonical = require('./canonical-json.cjs');
|
|
12
|
+
const changeAuthority = require('./change-authority.cjs');
|
|
12
13
|
const { writeManagedFile } = require('./managed-file-write.cjs');
|
|
13
14
|
const providerRefs = require('./provider-refs.cjs');
|
|
14
15
|
const baselines = require('./baseline-workflow.cjs');
|
|
@@ -30,10 +31,6 @@ function loadLegacyModule(filename) {
|
|
|
30
31
|
return module.require(path.join(__dirname, filename));
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
function legacyContextSpine() {
|
|
34
|
-
return loadLegacyModule('context-spine.cjs');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
34
|
function legacySpecLearning() {
|
|
38
35
|
return loadLegacyModule('spec-learning.cjs');
|
|
39
36
|
}
|
|
@@ -46,6 +43,10 @@ function legacyDecisions() {
|
|
|
46
43
|
return loadLegacyModule('decision-dialogue.cjs');
|
|
47
44
|
}
|
|
48
45
|
|
|
46
|
+
function legacyContextSpine() {
|
|
47
|
+
return loadLegacyModule('context-spine.cjs');
|
|
48
|
+
}
|
|
49
|
+
|
|
49
50
|
const CHANGE_ID = /^[a-zA-Z0-9_-]+$/;
|
|
50
51
|
const CHANGE_PATCH_FIELDS = new Set([
|
|
51
52
|
'title', 'intent', 'status', 'docs_impact', 'provider_refs',
|
|
@@ -1191,8 +1192,8 @@ function updateChange(db, id, patch = {}, {
|
|
|
1191
1192
|
const semanticFields = Object.keys(patch)
|
|
1192
1193
|
.filter((field) => SEMANTIC_AUTHORITY_FIELDS.has(field));
|
|
1193
1194
|
if (semanticFields.length > 0
|
|
1194
|
-
&&
|
|
1195
|
-
!==
|
|
1195
|
+
&& changeAuthority.changeStateDigest(current)
|
|
1196
|
+
!== changeAuthority.changeStateDigest(updated)) {
|
|
1196
1197
|
invalidated.push(...artifacts.invalidateConsumersFromEndpointInTx(
|
|
1197
1198
|
db,
|
|
1198
1199
|
{ type: 'change', id },
|
|
@@ -1354,6 +1355,60 @@ function generatePacketReconciliation(db, change, packet, rootDir) {
|
|
|
1354
1355
|
}
|
|
1355
1356
|
}
|
|
1356
1357
|
|
|
1358
|
+
function generateCallerNoChangeReconciliation(db, change, input, rootDir) {
|
|
1359
|
+
const baseline = baselines.readBaseline(db);
|
|
1360
|
+
const manifest = {
|
|
1361
|
+
$schema: 'ultra-baseline-reconciliation-v1',
|
|
1362
|
+
change_id: change.id,
|
|
1363
|
+
baseline_id: baseline?.id || null,
|
|
1364
|
+
baseline_updates: [],
|
|
1365
|
+
semantic_changes: [],
|
|
1366
|
+
semantic_no_change_reason: String(input.no_baseline_change_reason).trim(),
|
|
1367
|
+
resolved_gap_ids: [],
|
|
1368
|
+
resolved_unknowns: [],
|
|
1369
|
+
verification: [{
|
|
1370
|
+
name: 'explicit archive handoff',
|
|
1371
|
+
command: 'ultra.archive',
|
|
1372
|
+
status: 'pass',
|
|
1373
|
+
evidence: String(input.summary).trim(),
|
|
1374
|
+
}],
|
|
1375
|
+
};
|
|
1376
|
+
const relative = `${change.artifact_root}/baseline-reconciliation.json`;
|
|
1377
|
+
const serialized = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
1378
|
+
const file = safeRelativePath(rootDir, relative);
|
|
1379
|
+
const previous = fs.existsSync(file) ? fs.readFileSync(file) : null;
|
|
1380
|
+
const published = writeManagedFile(rootDir, relative, Buffer.from(serialized));
|
|
1381
|
+
try {
|
|
1382
|
+
const recorded = artifacts.recordArtifact(db, {
|
|
1383
|
+
id: `${change.id}-baseline-reconciliation`,
|
|
1384
|
+
owner_type: 'change',
|
|
1385
|
+
owner_id: change.id,
|
|
1386
|
+
kind: 'baseline_reconciliation',
|
|
1387
|
+
path: relative,
|
|
1388
|
+
content_digest: published.digest,
|
|
1389
|
+
source_refs: [],
|
|
1390
|
+
consumer_refs: [],
|
|
1391
|
+
provenance: { writer: 'ultra.archive', generated: true },
|
|
1392
|
+
metadata: {
|
|
1393
|
+
terminal_role: true,
|
|
1394
|
+
schema: manifest.$schema,
|
|
1395
|
+
caller_declared: true,
|
|
1396
|
+
},
|
|
1397
|
+
}, { rootDir }).artifact;
|
|
1398
|
+
return {
|
|
1399
|
+
manifest,
|
|
1400
|
+
relative,
|
|
1401
|
+
file,
|
|
1402
|
+
digest: recorded.digest,
|
|
1403
|
+
artifact: recorded,
|
|
1404
|
+
};
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
if (previous === null) fs.rmSync(file, { force: true });
|
|
1407
|
+
else fs.writeFileSync(file, previous);
|
|
1408
|
+
throw error;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1357
1412
|
function baselineConvergenceBlockers(db, change, rootDir) {
|
|
1358
1413
|
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
1359
1414
|
return legacyContextSpine().baselineGateForChange(db, change, baselineHealth).blockers;
|
|
@@ -2487,35 +2542,6 @@ function archiveChange(db, input, {
|
|
|
2487
2542
|
}
|
|
2488
2543
|
|
|
2489
2544
|
let packet = null;
|
|
2490
|
-
if (kernelMode) {
|
|
2491
|
-
const requiredStages = ['plan', 'test', 'review', 'deliver'];
|
|
2492
|
-
const missing = requiredStages.filter((stage) => !stageCheckpoints.currentCheckpoint(
|
|
2493
|
-
db,
|
|
2494
|
-
stage,
|
|
2495
|
-
{ change_id: change.id },
|
|
2496
|
-
{ includeDraft: false },
|
|
2497
|
-
));
|
|
2498
|
-
const tasks = ops.listTasks(db, { change_id: change.id });
|
|
2499
|
-
if (tasks.length === 0) missing.push('tasks');
|
|
2500
|
-
for (const task of tasks.filter((item) => item.status !== 'expanded')) {
|
|
2501
|
-
if (task.status !== 'completed') missing.push(`task:${task.id}:completed`);
|
|
2502
|
-
if (!stageCheckpoints.currentCheckpoint(
|
|
2503
|
-
db,
|
|
2504
|
-
'dev',
|
|
2505
|
-
{ task_id: task.id },
|
|
2506
|
-
{ includeDraft: false },
|
|
2507
|
-
)) {
|
|
2508
|
-
missing.push(`task:${task.id}:dev`);
|
|
2509
|
-
}
|
|
2510
|
-
}
|
|
2511
|
-
if (missing.length > 0) {
|
|
2512
|
-
throw new ChangeWorkflowError(
|
|
2513
|
-
'CHANGE_EVIDENCE_INCOMPLETE',
|
|
2514
|
-
`change ${change.id} lacks accepted delivery checkpoints`,
|
|
2515
|
-
{ missing },
|
|
2516
|
-
);
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
2519
2545
|
if (changePacket.readDeltaAuthority(db, change.id)) {
|
|
2520
2546
|
if (!kernelMode) {
|
|
2521
2547
|
legacyWorkflows().validateDeliveryPrerequisites(
|
|
@@ -2553,7 +2579,14 @@ function archiveChange(db, input, {
|
|
|
2553
2579
|
|
|
2554
2580
|
const reconciliation = packet
|
|
2555
2581
|
? generatePacketReconciliation(db, change, packet, rootDir)
|
|
2556
|
-
:
|
|
2582
|
+
: (
|
|
2583
|
+
kernelMode && updates.length === 0 && !String(input.reconciliation_path || '').trim()
|
|
2584
|
+
? generateCallerNoChangeReconciliation(db, change, {
|
|
2585
|
+
summary: input.summary,
|
|
2586
|
+
no_baseline_change_reason: noChangeReason,
|
|
2587
|
+
}, rootDir)
|
|
2588
|
+
: readReconciliationManifest(db, change, input, rootDir)
|
|
2589
|
+
);
|
|
2557
2590
|
if (kernelMode) {
|
|
2558
2591
|
materializeKernelArchiveAuthority(db, change, rootDir);
|
|
2559
2592
|
}
|
|
@@ -21,7 +21,7 @@ const STATUS_TRANSITIONS = Object.freeze({
|
|
|
21
21
|
in_progress: new Set(['completed', 'blocked', 'pending']),
|
|
22
22
|
blocked: new Set(['pending', 'in_progress']),
|
|
23
23
|
expanded: new Set(['completed']),
|
|
24
|
-
completed: new Set(),
|
|
24
|
+
completed: new Set(['in_progress']),
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
const TASK_FIELDS = Object.freeze([
|
|
@@ -529,6 +529,16 @@ function patchTask(db, id, patch = {}) {
|
|
|
529
529
|
`cannot transition task ${id} from ${current.status} to ${nextStatus}`,
|
|
530
530
|
);
|
|
531
531
|
}
|
|
532
|
+
if (current.status === 'completed' && nextStatus === 'in_progress') {
|
|
533
|
+
if (!Object.prototype.hasOwnProperty.call(patch, 'completion_commit')) {
|
|
534
|
+
sets.push('completion_commit = ?');
|
|
535
|
+
params.push(null);
|
|
536
|
+
}
|
|
537
|
+
if (!Object.prototype.hasOwnProperty.call(patch, 'session_id')) {
|
|
538
|
+
sets.push('session_id = ?');
|
|
539
|
+
params.push(null);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
532
542
|
sets.push('status = ?');
|
|
533
543
|
params.push(nextStatus);
|
|
534
544
|
}
|
|
@@ -542,10 +552,18 @@ function patchTask(db, id, patch = {}) {
|
|
|
542
552
|
|
|
543
553
|
if (nextStatus && nextStatus !== current.status) {
|
|
544
554
|
appendEventInTx(db, {
|
|
545
|
-
type:
|
|
555
|
+
type: current.status === 'completed' && nextStatus === 'in_progress'
|
|
556
|
+
? 'task_reopened'
|
|
557
|
+
: statusEventType(nextStatus),
|
|
546
558
|
task_id: id,
|
|
547
559
|
change_id: patch.change_id !== undefined ? patch.change_id : current.change_id,
|
|
548
|
-
payload: {
|
|
560
|
+
payload: {
|
|
561
|
+
from: current.status,
|
|
562
|
+
to: nextStatus,
|
|
563
|
+
...(current.status === 'completed' && nextStatus === 'in_progress'
|
|
564
|
+
? { previous_completion_commit: current.completion_commit }
|
|
565
|
+
: {}),
|
|
566
|
+
},
|
|
549
567
|
});
|
|
550
568
|
}
|
|
551
569
|
const contractFields = Object.keys(patch).filter((field) => TASK_CONTRACT_PATCH_FIELDS.has(field));
|