ultra-builder-pro-cli 0.25.0 → 0.26.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/.ultra-template/contexts/TEMPLATE.md +87 -0
- package/.ultra-template/decisions/.gitkeep +1 -0
- package/.ultra-template/evidence/.gitkeep +1 -0
- package/.ultra-template/north-star.md +46 -0
- package/.ultra-template/research/.gitkeep +1 -0
- package/.ultra-template/specs/discovery.md +2 -2
- package/.ultra-template/tasks.json +3 -0
- package/.ultra-template/test-report.json +27 -0
- package/CHANGELOG.md +49 -0
- package/README.md +217 -579
- package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
- package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
- package/adapters/_shared/file-ops.cjs +76 -2
- package/adapters/_shared/host-profile.cjs +97 -0
- package/adapters/_shared/plugin-core.cjs +204 -0
- package/adapters/_shared/runtime-assets.cjs +31 -96
- package/adapters/claude.js +92 -291
- package/adapters/codex.js +309 -842
- package/adapters/grok.js +143 -1469
- package/adapters/kimi.js +124 -1053
- package/adapters/opencode.js +230 -742
- package/bin/delegate-worker.cjs +323 -0
- package/bin/delegate.cjs +456 -0
- package/bin/install.js +38 -11
- package/docs/ARCHITECTURE.md +122 -216
- package/docs/ARTIFACT-AUTHORITY.md +90 -268
- package/docs/DECISIONS.md +75 -87
- package/docs/PHILOSOPHY.md +177 -0
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +62 -39
- package/docs/RUNTIME-COMPAT-MATRIX.md +99 -156
- package/docs/SKILL-AUTHORING.md +94 -73
- package/docs/WORKFLOW-LIFECYCLE.md +93 -189
- package/hooks/README.md +17 -37
- package/hooks/_common.py +113 -0
- package/hooks/adapters/codex.py +9 -16
- package/hooks/adapters/grok.py +5 -5
- package/hooks/adapters/kimi.py +8 -14
- package/hooks/block_dangerous_commands.py +95 -0
- package/hooks/compact_context.py +65 -0
- package/hooks/mid_workflow_recall.py +25 -0
- package/hooks/post_edit_guard.py +105 -0
- package/hooks/session_context.py +30 -0
- package/package.json +17 -52
- package/skills/ultra-change/SKILL.md +97 -50
- package/skills/ultra-change/references/change-contract.md +40 -1
- package/skills/ultra-delegate/SKILL.md +82 -0
- package/skills/ultra-delegate/references/delegation-contract.md +51 -0
- package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
- package/skills/ultra-deliver/SKILL.md +61 -42
- package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
- package/skills/ultra-dev/SKILL.md +96 -62
- package/skills/ultra-dev/references/debugging.md +22 -0
- package/skills/ultra-domain-modeling/SKILL.md +79 -0
- package/skills/ultra-grilling/SKILL.md +79 -0
- package/skills/ultra-init/SKILL.md +94 -37
- package/skills/ultra-init/references/brownfield-adoption.md +33 -83
- package/skills/ultra-init/scripts/init_project.cjs +84 -0
- package/skills/ultra-plan/SKILL.md +88 -68
- package/skills/ultra-research/SKILL.md +62 -48
- package/skills/ultra-research/references/00-problem-validation.md +5 -5
- package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
- package/skills/ultra-research/references/02-market-assessment.md +5 -4
- package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
- package/skills/ultra-research/references/04-product-strategy.md +3 -3
- package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
- package/skills/ultra-research/references/10-user-personas.md +3 -2
- package/skills/ultra-research/references/11-user-scenarios.md +3 -3
- package/skills/ultra-research/references/20-user-stories.md +4 -3
- package/skills/ultra-research/references/21-features-scope.md +3 -2
- package/skills/ultra-research/references/22-success-metrics.md +3 -3
- package/skills/ultra-research/references/30-architecture-context.md +3 -3
- package/skills/ultra-research/references/31-solution-strategy.md +3 -3
- package/skills/ultra-research/references/32-building-blocks.md +3 -3
- package/skills/ultra-research/references/40-deployment.md +3 -3
- package/skills/ultra-research/references/41-quality-risks.md +3 -3
- package/skills/ultra-research/references/99-synthesis.md +7 -7
- package/skills/ultra-review/SKILL.md +59 -32
- package/skills/ultra-review/references/code.md +17 -0
- package/skills/ultra-review/references/comments.md +12 -0
- package/skills/ultra-review/references/design.md +12 -0
- package/skills/ultra-review/references/errors.md +13 -0
- package/skills/ultra-review/references/spec.md +13 -0
- package/skills/ultra-review/references/tests.md +12 -0
- package/skills/ultra-review/references/worker-packet.md +40 -0
- package/skills/ultra-review/scripts/review_wait.py +26 -15
- package/skills/ultra-status/SKILL.md +61 -36
- package/skills/ultra-tdd/SKILL.md +81 -0
- package/skills/ultra-tdd/references/templates/README.md +20 -0
- package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
- package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
- package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
- package/skills/ultra-tdd/references/test-execution.md +18 -0
- package/skills/ultra-test/SKILL.md +70 -30
- package/skills/ultra-test/references/export-syntax.md +18 -0
- package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
- package/skills/ultra-think/SKILL.md +49 -27
- package/skills/ultra-think/references/autonomy-boundary.md +48 -0
- package/skills/ultra-think/references/interaction-boundary.md +18 -33
- package/.ultra-template/docs/research/README.md +0 -55
- package/.ultra-template/reports/templates/delivery-report.json +0 -17
- package/.ultra-template/reports/templates/test-report.json +0 -18
- package/.ultra-template/tasks/tasks.json +0 -15
- package/.ultra-template/templates/task-context.md +0 -19
- package/adapters/_shared/codex-assets.cjs +0 -725
- package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
- package/adapters/_shared/interaction-contract.cjs +0 -129
- package/adapters/_shared/path-rewrite.cjs +0 -79
- package/adapters/_shared/runtime-native.cjs +0 -132
- package/adapters/_shared/settings-merge.cjs +0 -76
- package/agents/code-reviewer.md +0 -42
- package/agents/debugger.md +0 -40
- package/agents/review-code.md +0 -49
- package/agents/review-comments.md +0 -38
- package/agents/review-coordinator.md +0 -56
- package/agents/review-design.md +0 -40
- package/agents/review-errors.md +0 -43
- package/agents/review-spec.md +0 -39
- package/agents/review-tests.md +0 -40
- package/agents/tdd-runner.md +0 -38
- package/bin/orchestrator.js +0 -383
- package/commands/ultra-change.md +0 -10
- package/commands/ultra-deliver.md +0 -10
- package/commands/ultra-dev.md +0 -10
- package/commands/ultra-doctor.md +0 -10
- package/commands/ultra-init.md +0 -10
- package/commands/ultra-plan.md +0 -10
- package/commands/ultra-research.md +0 -10
- package/commands/ultra-review.md +0 -10
- package/commands/ultra-status.md +0 -10
- package/commands/ultra-test.md +0 -10
- package/commands/ultra-think.md +0 -10
- package/docs/AGENT-CONTEXT.md +0 -191
- package/docs/COMMIT-HASH-BACKFILL.md +0 -51
- package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
- package/docs/ROADMAP.md +0 -101
- package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
- package/hooks/active_task_context.py +0 -152
- package/hooks/context_envelope.py +0 -100
- package/hooks/health_check.py +0 -196
- package/hooks/pre_stop_check.py +0 -66
- package/hooks/runtime_paths.py +0 -396
- package/hooks/subagent_tracker.py +0 -88
- package/hooks/workflow_checkpoint.py +0 -76
- package/hooks/workflow_context.py +0 -48
- package/hooks/workflow_resume.py +0 -84
- package/mcp-server/hook-context.cjs +0 -38
- package/mcp-server/hook-event.cjs +0 -34
- package/mcp-server/lib/archive-journal.cjs +0 -1119
- package/mcp-server/lib/archive-mutation-worker.py +0 -365
- package/mcp-server/lib/artifact-registry.cjs +0 -1232
- package/mcp-server/lib/baseline-workflow.cjs +0 -1193
- package/mcp-server/lib/canonical-json.cjs +0 -19
- package/mcp-server/lib/change-authority.cjs +0 -26
- package/mcp-server/lib/change-packet.cjs +0 -790
- package/mcp-server/lib/change-workflow.cjs +0 -65
- package/mcp-server/lib/context-envelope.cjs +0 -581
- package/mcp-server/lib/context-paths.cjs +0 -142
- package/mcp-server/lib/decision-records.cjs +0 -423
- package/mcp-server/lib/delivery-transaction.cjs +0 -471
- package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
- package/mcp-server/lib/doctor.cjs +0 -627
- package/mcp-server/lib/git-bootstrap.cjs +0 -651
- package/mcp-server/lib/init-project.cjs +0 -1021
- package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
- package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
- package/mcp-server/lib/managed-file-write.cjs +0 -172
- package/mcp-server/lib/plan-checkpoint.cjs +0 -177
- package/mcp-server/lib/plan-publication-worker.cjs +0 -830
- package/mcp-server/lib/plan-store.cjs +0 -794
- package/mcp-server/lib/prd-parser.cjs +0 -83
- package/mcp-server/lib/pricing.cjs +0 -44
- package/mcp-server/lib/project-context.cjs +0 -178
- package/mcp-server/lib/projector.cjs +0 -386
- package/mcp-server/lib/provider-refs.cjs +0 -42
- package/mcp-server/lib/runtime-paths.cjs +0 -1687
- package/mcp-server/lib/runtime-state.cjs +0 -222
- package/mcp-server/lib/safe-project-file.cjs +0 -385
- package/mcp-server/lib/stage-checkpoints.cjs +0 -463
- package/mcp-server/lib/state-authority.cjs +0 -95
- package/mcp-server/lib/state-db.cjs +0 -1580
- package/mcp-server/lib/state-ops.cjs +0 -1428
- package/mcp-server/lib/task-contract.cjs +0 -404
- package/mcp-server/lib/task-expander.cjs +0 -105
- package/mcp-server/lib/task-ledger.cjs +0 -1964
- package/mcp-server/lib/telemetry.cjs +0 -85
- package/mcp-server/lib/topo.cjs +0 -131
- package/mcp-server/lib/ultra-facade.cjs +0 -2684
- package/mcp-server/lib/worker-packet.cjs +0 -358
- package/mcp-server/server.cjs +0 -437
- package/orchestrator/auto-merge.cjs +0 -128
- package/orchestrator/daemon.cjs +0 -264
- package/orchestrator/dispatch-rules.cjs +0 -78
- package/orchestrator/parallel-orchestrator.cjs +0 -903
- package/orchestrator/planner/plan-builder.cjs +0 -125
- package/orchestrator/recovery.cjs +0 -188
- package/orchestrator/session-close-journal-worker.cjs +0 -5
- package/orchestrator/session-close-journal.cjs +0 -1014
- package/orchestrator/session-runner.cjs +0 -1153
- package/orchestrator/worktree-manager.cjs +0 -282
- package/settings.json +0 -7
- package/skills/cc-collab/SKILL.md +0 -42
- package/skills/code-review-expert/SKILL.md +0 -43
- package/skills/code-review-expert/references/correctness-reliability.md +0 -30
- package/skills/code-review-expert/references/design-boundaries.md +0 -26
- package/skills/code-review-expert/references/removal-plan.md +0 -24
- package/skills/codex-collab/SKILL.md +0 -49
- package/skills/codex-collab/evals/evals.json +0 -35
- package/skills/integration-rules/SKILL.md +0 -32
- package/skills/security-rules/SKILL.md +0 -38
- package/skills/testing-rules/SKILL.md +0 -32
- package/skills/ultra-doctor/SKILL.md +0 -41
- package/skills/ultra-plan/references/semantic-preflight.md +0 -20
- package/skills/ultra-verify/SKILL.md +0 -46
- package/skills/ultra-verify/evals/evals.json +0 -46
- package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
- package/skills/ultra-verify/references/evidence-status.md +0 -21
- package/skills/ultra-verify/scripts/verify_wait.py +0 -94
- package/spec/cli-protocol.md +0 -102
- package/spec/mcp-tools.yaml +0 -312
- package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
- package/spec/schemas/change-delta.v1.schema.json +0 -153
- package/spec/schemas/command-manifest.schema.json +0 -29
- package/spec/schemas/context-file.v4.5.schema.json +0 -54
- package/spec/schemas/delivery-report.v1.schema.json +0 -66
- package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
- package/spec/schemas/mcp-tools.schema.json +0 -136
- package/spec/schemas/skill-manifest.schema.json +0 -24
- package/spec/schemas/state-db.sql +0 -641
- package/spec/schemas/task-ledger.v2.schema.json +0 -296
- package/spec/schemas/tasks.v4.5.schema.json +0 -96
- package/spec/schemas/test-report.v1.schema.json +0 -225
- package/ultra-tools/cli.cjs +0 -129
- package/ultra-tools/commands/db.cjs +0 -226
- package/ultra-tools/commands/legacy-memory.cjs +0 -159
- package/ultra-tools/commands/migrate.cjs +0 -1386
- package/ultra-tools/commands/session.cjs +0 -213
- package/ultra-tools/commands/status.cjs +0 -559
- package/ultra-tools/commands/system.cjs +0 -498
- package/ultra-tools/commands/task.cjs +0 -105
|
@@ -1,2029 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const crypto = require('node:crypto');
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const {spawnSync} = require('node:child_process');
|
|
6
|
-
const Ajv = require('ajv/dist/2020');
|
|
7
|
-
const ops = require('./state-ops.cjs');
|
|
8
|
-
const artifacts = require('./artifact-registry.cjs');
|
|
9
|
-
const canonical = require('./canonical-json.cjs');
|
|
10
|
-
const changeAuthority = require('./change-authority.cjs');
|
|
11
|
-
const {writeManagedFile} = require('./managed-file-write.cjs');
|
|
12
|
-
const providerRefs = require('./provider-refs.cjs');
|
|
13
|
-
const baselines = require('./baseline-workflow.cjs');
|
|
14
|
-
const archiveJournal = require('./archive-journal.cjs');
|
|
15
|
-
const changePacket = require('./change-packet.cjs');
|
|
16
|
-
const deliveryTransaction = require('./delivery-transaction.cjs');
|
|
17
|
-
const {openStableProjectRead, readStableProjectFile, walkStableProjectTree} = require('./safe-project-file.cjs');
|
|
18
|
-
const stageCheckpoints = require('./stage-checkpoints.cjs');
|
|
19
|
-
const reconciliationSchema = require('../../spec/schemas/baseline-reconciliation.v1.schema.json');
|
|
20
|
-
const reconciliationAjv = new Ajv({
|
|
21
|
-
allErrors: true,
|
|
22
|
-
strict: false
|
|
23
|
-
});
|
|
24
|
-
const validateReconciliationSchema = reconciliationAjv.compile(reconciliationSchema);
|
|
25
|
-
const CHANGE_ID = /^[a-zA-Z0-9_-]+$/;
|
|
26
|
-
const CHANGE_PATCH_FIELDS = new Set([
|
|
27
|
-
'title',
|
|
28
|
-
'intent',
|
|
29
|
-
'status',
|
|
30
|
-
'docs_impact',
|
|
31
|
-
'provider_refs',
|
|
32
|
-
'contract',
|
|
33
|
-
'classification',
|
|
34
|
-
'research_disposition'
|
|
35
|
-
]);
|
|
36
|
-
const CHANGE_CREATE_FIELDS = new Set([
|
|
37
|
-
'id',
|
|
38
|
-
'title',
|
|
39
|
-
'kind',
|
|
40
|
-
'intent',
|
|
41
|
-
'docs_impact',
|
|
42
|
-
'provider_refs',
|
|
43
|
-
'baseline_bypass',
|
|
44
|
-
'contract',
|
|
45
|
-
'classification',
|
|
46
|
-
'research_disposition',
|
|
47
|
-
'base_commit',
|
|
48
|
-
'supersedes_id'
|
|
49
|
-
]);
|
|
50
|
-
const SEMANTIC_AUTHORITY_FIELDS = new Set([
|
|
51
|
-
'intent',
|
|
52
|
-
'docs_impact',
|
|
53
|
-
'provider_refs',
|
|
54
|
-
'contract',
|
|
55
|
-
'classification',
|
|
56
|
-
'research_disposition'
|
|
57
|
-
]);
|
|
58
|
-
const INCIDENT_DIAGNOSIS_SECTIONS = Object.freeze([
|
|
59
|
-
{
|
|
60
|
-
heading: 'Reproduction',
|
|
61
|
-
key: 'reproduction'
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
heading: 'Hypotheses',
|
|
65
|
-
key: 'hypotheses'
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
heading: 'Root cause',
|
|
69
|
-
key: 'root-cause'
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
heading: 'Regression test',
|
|
73
|
-
key: 'regression-test'
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
heading: 'Recovery',
|
|
77
|
-
key: 'recovery'
|
|
78
|
-
}
|
|
79
|
-
]);
|
|
80
|
-
const LIST_CHANGES_SQL = 'SELECT * FROM changes WHERE (@status IS NULL OR status = @status) AND (@kind IS NULL OR kind = @kind) ORDER BY created_at DESC LIMIT @maxn';
|
|
81
|
-
class ChangeWorkflowError extends Error {
|
|
82
|
-
constructor(code, message, details) {
|
|
83
|
-
super(message);
|
|
84
|
-
this.name = 'ChangeWorkflowError';
|
|
85
|
-
this.code = code;
|
|
86
|
-
if (details !== undefined)
|
|
87
|
-
this.details = details;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
function nowIso() {
|
|
91
|
-
return new Date().toISOString();
|
|
92
|
-
}
|
|
93
|
-
function parseJson(value, field) {
|
|
94
|
-
try {
|
|
95
|
-
return JSON.parse(value);
|
|
96
|
-
} catch (error) {
|
|
97
|
-
throw new ChangeWorkflowError('STATE_CORRUPT', `invalid ${ field }: ${ error.message }`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
function rowToChange(row) {
|
|
101
|
-
if (!row)
|
|
102
|
-
return null;
|
|
103
|
-
const change = {
|
|
104
|
-
...row,
|
|
105
|
-
docs_impact: parseJson(row.docs_impact_json, 'docs_impact_json'),
|
|
106
|
-
provider_refs: parseJson(row.provider_refs_json, 'provider_refs_json'),
|
|
107
|
-
baseline_bypass: row.baseline_bypass_json ? parseJson(row.baseline_bypass_json, 'baseline_bypass_json') : null,
|
|
108
|
-
contract: parseJson(row.contract_json || '{}', 'contract_json'),
|
|
109
|
-
classification: parseJson(row.classification_json || '{}', 'classification_json'),
|
|
110
|
-
research_disposition: parseJson(row.research_disposition_json || '{}', 'research_disposition_json')
|
|
111
|
-
};
|
|
112
|
-
delete change.docs_impact_json;
|
|
113
|
-
delete change.provider_refs_json;
|
|
114
|
-
delete change.baseline_bypass_json;
|
|
115
|
-
delete change.contract_json;
|
|
116
|
-
delete change.classification_json;
|
|
117
|
-
delete change.research_disposition_json;
|
|
118
|
-
return change;
|
|
119
|
-
}
|
|
120
|
-
function assertExactFields(value, allowed, field) {
|
|
121
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
122
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', `${ field } must be an object`);
|
|
123
|
-
}
|
|
124
|
-
const unknown = Object.keys(value).filter(key => !allowed.has(key));
|
|
125
|
-
if (unknown.length > 0) {
|
|
126
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', `${ field }.${ unknown[0] } is not allowed`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
function normalizeDocsImpact(value) {
|
|
130
|
-
const input = value === undefined ? {} : value;
|
|
131
|
-
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
132
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', 'docs impact must be an object');
|
|
133
|
-
}
|
|
134
|
-
assertExactFields(input, new Set([
|
|
135
|
-
'status',
|
|
136
|
-
'files',
|
|
137
|
-
'rationale'
|
|
138
|
-
]), 'docs_impact');
|
|
139
|
-
const status = input.status === undefined ? 'unknown' : input.status;
|
|
140
|
-
if (typeof status !== 'string') {
|
|
141
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', 'docs impact status must be a string');
|
|
142
|
-
}
|
|
143
|
-
if (![
|
|
144
|
-
'unknown',
|
|
145
|
-
'required',
|
|
146
|
-
'none'
|
|
147
|
-
].includes(status)) {
|
|
148
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', `invalid docs impact status: ${ status }`);
|
|
149
|
-
}
|
|
150
|
-
const files = input.files === undefined ? [] : input.files;
|
|
151
|
-
if (!Array.isArray(files) || files.some(file => typeof file !== 'string' || !file.trim())) {
|
|
152
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', 'docs impact files must be non-empty strings');
|
|
153
|
-
}
|
|
154
|
-
if (input.rationale != null && typeof input.rationale !== 'string') {
|
|
155
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', 'docs impact rationale must be a string or null');
|
|
156
|
-
}
|
|
157
|
-
const rationale = input.rationale == null ? null : input.rationale.trim();
|
|
158
|
-
return {
|
|
159
|
-
status,
|
|
160
|
-
files: [...new Set(files.map(file => file.trim()))],
|
|
161
|
-
rationale
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
function normalizeProviderRefs(value) {
|
|
165
|
-
return providerRefs.normalizeProviderRefs(value, ChangeWorkflowError);
|
|
166
|
-
}
|
|
167
|
-
function requiredText(value, field) {
|
|
168
|
-
const text = typeof value === 'string' ? value.trim() : '';
|
|
169
|
-
if (!text)
|
|
170
|
-
throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `${ field } is required`);
|
|
171
|
-
return text;
|
|
172
|
-
}
|
|
173
|
-
function stringList(value, field, {
|
|
174
|
-
allowEmpty = false
|
|
175
|
-
} = {}) {
|
|
176
|
-
if (!Array.isArray(value) || !allowEmpty && value.length === 0 || value.some(item => typeof item !== 'string' || !item.trim())) {
|
|
177
|
-
throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `${ field } must be ${ allowEmpty ? 'an' : 'a non-empty' } array of strings`);
|
|
178
|
-
}
|
|
179
|
-
return [...new Set(value.map(item => item.trim()))];
|
|
180
|
-
}
|
|
181
|
-
function optionalTrimmedText(value, field, {
|
|
182
|
-
maxLength = 4000
|
|
183
|
-
} = {}) {
|
|
184
|
-
if (value === undefined || value === null)
|
|
185
|
-
return '';
|
|
186
|
-
if (typeof value !== 'string') {
|
|
187
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', `${ field } must be a string`);
|
|
188
|
-
}
|
|
189
|
-
const text = value.trim();
|
|
190
|
-
if (text.length > maxLength) {
|
|
191
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', `${ field } must not exceed ${ maxLength } characters`);
|
|
192
|
-
}
|
|
193
|
-
return text;
|
|
194
|
-
}
|
|
195
|
-
function boundedVocabulary(value, field) {
|
|
196
|
-
const text = optionalTrimmedText(value, field, { maxLength: 80 });
|
|
197
|
-
if (!text) {
|
|
198
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', `${ field } is required`);
|
|
199
|
-
}
|
|
200
|
-
return text;
|
|
201
|
-
}
|
|
202
|
-
function normalizeChangeContract(value) {
|
|
203
|
-
const input = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
204
|
-
assertExactFields(input, new Set([
|
|
205
|
-
'outcome',
|
|
206
|
-
'acceptance',
|
|
207
|
-
'non_goals',
|
|
208
|
-
'public_seams',
|
|
209
|
-
'recovery',
|
|
210
|
-
'unresolved_decisions'
|
|
211
|
-
]), 'contract');
|
|
212
|
-
if (input.acceptance !== undefined && !Array.isArray(input.acceptance)) {
|
|
213
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', 'contract.acceptance must be an array');
|
|
214
|
-
}
|
|
215
|
-
const ids = new Set();
|
|
216
|
-
const acceptance = (input.acceptance || []).map((item, index) => {
|
|
217
|
-
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
218
|
-
throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `contract.acceptance[${ index }] is invalid`);
|
|
219
|
-
}
|
|
220
|
-
assertExactFields(item, new Set([
|
|
221
|
-
'id',
|
|
222
|
-
'criterion',
|
|
223
|
-
'verification'
|
|
224
|
-
]), `contract.acceptance[${ index }]`);
|
|
225
|
-
const id = requiredText(item.id, `contract.acceptance[${ index }].id`);
|
|
226
|
-
if (!CHANGE_ID.test(id) || ids.has(id)) {
|
|
227
|
-
throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `acceptance id is invalid or duplicated: ${ id }`);
|
|
228
|
-
}
|
|
229
|
-
ids.add(id);
|
|
230
|
-
return {
|
|
231
|
-
id,
|
|
232
|
-
criterion: requiredText(item.criterion, `contract.acceptance[${ index }].criterion`),
|
|
233
|
-
verification: requiredText(item.verification, `contract.acceptance[${ index }].verification`)
|
|
234
|
-
};
|
|
235
|
-
});
|
|
236
|
-
const recovery = input.recovery;
|
|
237
|
-
if (recovery !== undefined && recovery !== null) {
|
|
238
|
-
assertExactFields(recovery, new Set([
|
|
239
|
-
'strategy',
|
|
240
|
-
'verification'
|
|
241
|
-
]), 'contract.recovery');
|
|
242
|
-
}
|
|
243
|
-
if (input.unresolved_decisions !== undefined && !Array.isArray(input.unresolved_decisions)) {
|
|
244
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', 'contract.unresolved_decisions must be an array');
|
|
245
|
-
}
|
|
246
|
-
const decisions = (input.unresolved_decisions || []).map((item, index) => {
|
|
247
|
-
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
248
|
-
throw new ChangeWorkflowError('CHANGE_CONTRACT_REQUIRED', `unresolved decision ${ index } is invalid`);
|
|
249
|
-
}
|
|
250
|
-
assertExactFields(item, new Set([
|
|
251
|
-
'id',
|
|
252
|
-
'summary',
|
|
253
|
-
'blocking',
|
|
254
|
-
'owner'
|
|
255
|
-
]), `contract.unresolved_decisions[${ index }]`);
|
|
256
|
-
if (item.blocking !== undefined && typeof item.blocking !== 'boolean') {
|
|
257
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', `contract.unresolved_decisions[${ index }].blocking must be a boolean`);
|
|
258
|
-
}
|
|
259
|
-
return {
|
|
260
|
-
id: requiredText(item.id, `contract.unresolved_decisions[${ index }].id`),
|
|
261
|
-
summary: requiredText(item.summary, `contract.unresolved_decisions[${ index }].summary`),
|
|
262
|
-
blocking: item.blocking === true,
|
|
263
|
-
owner: item.owner == null ? null : requiredText(item.owner, `contract.unresolved_decisions[${ index }].owner`)
|
|
264
|
-
};
|
|
265
|
-
});
|
|
266
|
-
return {
|
|
267
|
-
outcome: optionalTrimmedText(input.outcome, 'contract.outcome'),
|
|
268
|
-
acceptance,
|
|
269
|
-
non_goals: stringList(input.non_goals || [], 'contract.non_goals', { allowEmpty: true }),
|
|
270
|
-
public_seams: stringList(input.public_seams || [], 'contract.public_seams', { allowEmpty: true }),
|
|
271
|
-
recovery: recovery ? {
|
|
272
|
-
strategy: optionalTrimmedText(recovery.strategy, 'contract.recovery.strategy'),
|
|
273
|
-
verification: optionalTrimmedText(recovery.verification, 'contract.recovery.verification')
|
|
274
|
-
} : null,
|
|
275
|
-
unresolved_decisions: decisions
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
function normalizeClassification(value) {
|
|
279
|
-
const input = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
280
|
-
assertExactFields(input, new Set([
|
|
281
|
-
'rationale',
|
|
282
|
-
'risk_flags'
|
|
283
|
-
]), 'classification');
|
|
284
|
-
const flags = stringList(input.risk_flags === undefined ? [] : input.risk_flags, 'classification.risk_flags', { allowEmpty: true });
|
|
285
|
-
return {
|
|
286
|
-
rationale: optionalTrimmedText(input.rationale, 'classification.rationale'),
|
|
287
|
-
risk_flags: flags
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
function normalizeResearchDisposition(value) {
|
|
291
|
-
const input = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
292
|
-
assertExactFields(input, new Set([
|
|
293
|
-
'status',
|
|
294
|
-
'mode',
|
|
295
|
-
'selected_steps',
|
|
296
|
-
'rationale'
|
|
297
|
-
]), 'research_disposition');
|
|
298
|
-
const status = input.status === undefined ? 'unresolved' : boundedVocabulary(input.status, 'research_disposition.status');
|
|
299
|
-
const selectedSteps = stringList(input.selected_steps === undefined ? [] : input.selected_steps, 'research_disposition.selected_steps', { allowEmpty: true });
|
|
300
|
-
if (input.mode !== undefined && input.mode !== null && typeof input.mode !== 'string') {
|
|
301
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', 'research_disposition.mode must be a string or null');
|
|
302
|
-
}
|
|
303
|
-
const mode = input.mode == null ? null : boundedVocabulary(input.mode, 'research_disposition.mode');
|
|
304
|
-
return {
|
|
305
|
-
status,
|
|
306
|
-
mode,
|
|
307
|
-
selected_steps: selectedSteps,
|
|
308
|
-
rationale: optionalTrimmedText(input.rationale, 'research_disposition.rationale')
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
function normalizeBaselineBypass(value) {
|
|
312
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
313
|
-
throw new ChangeWorkflowError('BASELINE_BYPASS_REQUIRED', 'incident work without a ready baseline requires an explicit baseline_bypass record');
|
|
314
|
-
}
|
|
315
|
-
assertExactFields(value, new Set([
|
|
316
|
-
'reason',
|
|
317
|
-
'approved_by'
|
|
318
|
-
]), 'baseline_bypass');
|
|
319
|
-
const reason = typeof value.reason === 'string' ? value.reason.trim() : '';
|
|
320
|
-
const approvedBy = typeof value.approved_by === 'string' ? value.approved_by.trim() : '';
|
|
321
|
-
if (reason.length < 3 || !approvedBy) {
|
|
322
|
-
throw new ChangeWorkflowError('BASELINE_BYPASS_REQUIRED', 'baseline_bypass requires reason and approved_by');
|
|
323
|
-
}
|
|
324
|
-
return {
|
|
325
|
-
mode: 'incident_break_glass',
|
|
326
|
-
reason,
|
|
327
|
-
approved_by: approvedBy,
|
|
328
|
-
recorded_at: nowIso()
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
function gitHead(rootDir) {
|
|
332
|
-
const result = spawnSync('git', [
|
|
333
|
-
'rev-parse',
|
|
334
|
-
'HEAD'
|
|
335
|
-
], {
|
|
336
|
-
cwd: rootDir,
|
|
337
|
-
encoding: 'utf8',
|
|
338
|
-
stdio: [
|
|
339
|
-
'ignore',
|
|
340
|
-
'pipe',
|
|
341
|
-
'pipe'
|
|
342
|
-
]
|
|
343
|
-
});
|
|
344
|
-
return result.status === 0 ? result.stdout.trim() : null;
|
|
345
|
-
}
|
|
346
|
-
function safeRelativePath(rootDir, candidate) {
|
|
347
|
-
if (typeof candidate !== 'string' || !candidate.trim() || path.isAbsolute(candidate)) {
|
|
348
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', `path must be project-relative: ${ candidate }`);
|
|
349
|
-
}
|
|
350
|
-
const root = path.resolve(rootDir);
|
|
351
|
-
const target = path.resolve(root, candidate);
|
|
352
|
-
if (target !== root && !target.startsWith(`${ root }${ path.sep }`)) {
|
|
353
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', `path escapes project root: ${ candidate }`);
|
|
354
|
-
}
|
|
355
|
-
return target;
|
|
356
|
-
}
|
|
357
|
-
function markdownSlug(value) {
|
|
358
|
-
return String(value).trim().toLowerCase().replace(/<[^>]+>/g, '').replace(/[^\p{L}\p{N}\s-]/gu, '').replace(/\s+/g, '-').replace(/-+/g, '-');
|
|
359
|
-
}
|
|
360
|
-
function resolveProjectSourceRef(rootDir, sourceRef, field) {
|
|
361
|
-
const ref = typeof sourceRef === 'string' ? sourceRef.trim() : '';
|
|
362
|
-
const separator = ref.lastIndexOf('#');
|
|
363
|
-
if (separator <= 0 || separator === ref.length - 1) {
|
|
364
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', `${ field } must use path#anchor`);
|
|
365
|
-
}
|
|
366
|
-
const relative = artifacts.normalizeRelativePath(ref.slice(0, separator));
|
|
367
|
-
const anchor = ref.slice(separator + 1).trim().toLowerCase();
|
|
368
|
-
const resolved = readStableProjectFile(rootDir, relative, { encoding: 'utf8' });
|
|
369
|
-
const anchors = new Set();
|
|
370
|
-
for (const match of resolved.text.matchAll(/^#{1,6}\s+(.+?)\s*#*\s*$/gm)) {
|
|
371
|
-
anchors.add(markdownSlug(match[1]));
|
|
372
|
-
}
|
|
373
|
-
for (const match of resolved.text.matchAll(/<a\s+(?:name|id)=["']([^"']+)["'][^>]*>/gi)) {
|
|
374
|
-
anchors.add(match[1].trim().toLowerCase());
|
|
375
|
-
}
|
|
376
|
-
if (!anchors.has(anchor)) {
|
|
377
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', `${ field } anchor does not exist: ${ ref }`);
|
|
378
|
-
}
|
|
379
|
-
return {
|
|
380
|
-
ref,
|
|
381
|
-
relative,
|
|
382
|
-
anchor,
|
|
383
|
-
digest: resolved.digest
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
function rebindRegistryPath(candidate, source, destination) {
|
|
387
|
-
const current = artifacts.normalizeRelativePath(candidate);
|
|
388
|
-
const from = artifacts.normalizeRelativePath(source);
|
|
389
|
-
const to = artifacts.normalizeRelativePath(destination);
|
|
390
|
-
if (current !== from && !current.startsWith(`${ from }/`))
|
|
391
|
-
return null;
|
|
392
|
-
const suffix = current === from ? '' : path.posix.relative(from, current);
|
|
393
|
-
return suffix ? path.posix.join(to, suffix) : to;
|
|
394
|
-
}
|
|
395
|
-
function assertReconciliationAuthority(db, change, relative, digest) {
|
|
396
|
-
const rows = db.prepare('SELECT * FROM artifacts WHERE status <> \'archived\' ORDER BY id').all().filter(item => {
|
|
397
|
-
try {
|
|
398
|
-
return artifacts.normalizeRelativePath(item.path) === relative;
|
|
399
|
-
} catch {
|
|
400
|
-
return false;
|
|
401
|
-
}
|
|
402
|
-
});
|
|
403
|
-
const row = rows.length === 1 ? rows[0] : null;
|
|
404
|
-
const authorityDigest = row?.digest || row?.content_hash || null;
|
|
405
|
-
if (!row || row.managed !== 1 || row.owner_type !== 'change' || row.owner_id !== change.id || row.change_id !== change.id || row.kind !== 'baseline_reconciliation' || row.status !== 'current' || authorityDigest !== digest) {
|
|
406
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_AUTHORITY_INVALID', 'reconciliation manifest must match one current managed Change artifact authority', {
|
|
407
|
-
path: relative,
|
|
408
|
-
artifact_ids: rows.map(item => item.id),
|
|
409
|
-
expected_owner: `change:${ change.id }`,
|
|
410
|
-
expected_kind: 'baseline_reconciliation',
|
|
411
|
-
expected_digest: digest,
|
|
412
|
-
actual_digest: authorityDigest
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
return row;
|
|
416
|
-
}
|
|
417
|
-
function openReconciliationReader(rootDir, relative) {
|
|
418
|
-
try {
|
|
419
|
-
return openStableProjectRead(rootDir, relative);
|
|
420
|
-
} catch (error) {
|
|
421
|
-
if (error?.code === 'PROJECT_FILE_MISSING')
|
|
422
|
-
return null;
|
|
423
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_UNSAFE', `reconciliation manifest cannot be read safely: ${ relative }`, { cause: error.code || error.message });
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
function readReconciliationManifest(db, change, input, rootDir) {
|
|
427
|
-
const requested = typeof input.reconciliation_path === 'string' ? input.reconciliation_path.trim() : '';
|
|
428
|
-
if (!requested) {
|
|
429
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_REQUIRED', 'reconciliation_path is required');
|
|
430
|
-
}
|
|
431
|
-
let relative;
|
|
432
|
-
try {
|
|
433
|
-
relative = artifacts.normalizeRelativePath(requested);
|
|
434
|
-
} catch (error) {
|
|
435
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_REQUIRED', error.message);
|
|
436
|
-
}
|
|
437
|
-
let file = safeRelativePath(rootDir, relative);
|
|
438
|
-
const artifactRoot = path.resolve(rootDir, change.artifact_root);
|
|
439
|
-
if (!file.startsWith(`${ artifactRoot }${ path.sep }`)) {
|
|
440
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_REQUIRED', 'reconciliation manifest must be a file inside the active change artifact root');
|
|
441
|
-
}
|
|
442
|
-
let reader = openReconciliationReader(rootDir, relative);
|
|
443
|
-
let manifest;
|
|
444
|
-
let digest;
|
|
445
|
-
let expectedDigest = null;
|
|
446
|
-
let authorityDigest = null;
|
|
447
|
-
let journalIntent = null;
|
|
448
|
-
if (!reader) {
|
|
449
|
-
const pending = archiveJournal.listArchiveIntents(rootDir).find(item => !item.error && item.intent.change_id === change.id && item.intent.reconciliation_path === relative);
|
|
450
|
-
if (pending) {
|
|
451
|
-
journalIntent = pending.intent;
|
|
452
|
-
const suffix = path.posix.relative(pending.intent.source, relative);
|
|
453
|
-
const archivedRelative = path.posix.join(pending.intent.destination, suffix);
|
|
454
|
-
file = safeRelativePath(rootDir, archivedRelative);
|
|
455
|
-
reader = openReconciliationReader(rootDir, archivedRelative);
|
|
456
|
-
expectedDigest = pending.intent.reconciliation_digest;
|
|
457
|
-
authorityDigest = pending.intent.reconciliation_digest;
|
|
458
|
-
const rebind = archiveJournal.readArchiveRebind(rootDir, pending.intent);
|
|
459
|
-
const rebound = rebind?.entries.find(entry => entry.relative_path === path.posix.relative(pending.intent.destination, archivedRelative));
|
|
460
|
-
if (rebound) {
|
|
461
|
-
if (rebound.before_digest !== pending.intent.reconciliation_digest) {
|
|
462
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_AUTHORITY_INVALID', 'archive rebind journal does not begin from the prepared reconciliation authority', {
|
|
463
|
-
expected_digest: pending.intent.reconciliation_digest,
|
|
464
|
-
actual_digest: rebound.before_digest
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
expectedDigest = rebound.after_digest;
|
|
468
|
-
authorityDigest = rebound.before_digest;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
if (!reader) {
|
|
473
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_REQUIRED', 'reconciliation manifest file is missing');
|
|
474
|
-
}
|
|
475
|
-
try {
|
|
476
|
-
digest = reader.digest;
|
|
477
|
-
if (expectedDigest && digest !== expectedDigest) {
|
|
478
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_AUTHORITY_INVALID', 'archived reconciliation bytes do not match the prepared journal digest', {
|
|
479
|
-
expected_digest: expectedDigest,
|
|
480
|
-
actual_digest: digest
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
assertReconciliationAuthority(db, change, relative, authorityDigest || digest);
|
|
484
|
-
manifest = JSON.parse(reader.bytes.toString('utf8'));
|
|
485
|
-
reader.verify();
|
|
486
|
-
} catch (error) {
|
|
487
|
-
if (error instanceof ChangeWorkflowError)
|
|
488
|
-
throw error;
|
|
489
|
-
if (error?.code === 'PROJECT_FILE_CHANGED' || error?.code === 'PROJECT_FILE_UNSAFE') {
|
|
490
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_UNSAFE', `reconciliation manifest changed while it was read: ${ relative }`, { cause: error.code });
|
|
491
|
-
}
|
|
492
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', error.message);
|
|
493
|
-
} finally {
|
|
494
|
-
reader.close();
|
|
495
|
-
}
|
|
496
|
-
if (!validateReconciliationSchema(manifest)) {
|
|
497
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', `reconciliation manifest does not satisfy its published schema: ${ reconciliationAjv.errorsText(validateReconciliationSchema.errors) }`, validateReconciliationSchema.errors);
|
|
498
|
-
}
|
|
499
|
-
const baseline = baselines.readBaseline(db);
|
|
500
|
-
const updates = Array.isArray(input.baseline_updates) ? input.baseline_updates : [];
|
|
501
|
-
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest) || manifest.$schema !== 'ultra-baseline-reconciliation-v1' || manifest.change_id !== change.id || manifest.baseline_id !== (baseline?.id || null) || JSON.stringify(manifest.baseline_updates) !== JSON.stringify(updates) || !Array.isArray(manifest.semantic_changes) || !Array.isArray(manifest.resolved_gap_ids) || !Array.isArray(manifest.resolved_unknowns) || !Array.isArray(manifest.verification) || manifest.verification.length === 0) {
|
|
502
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', 'reconciliation manifest shape or authority binding is invalid');
|
|
503
|
-
}
|
|
504
|
-
if (manifest.verification.some(item => !item || typeof item !== 'object' || item.status !== 'pass' || !String(item.name || '').trim() || !String(item.command || '').trim() || !String(item.evidence || '').trim())) {
|
|
505
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', 'every reconciliation verification must be an evidenced pass');
|
|
506
|
-
}
|
|
507
|
-
if (updates.length === 0) {
|
|
508
|
-
if (manifest.semantic_changes.length > 0 || String(manifest.semantic_no_change_reason || '').trim().length < 3) {
|
|
509
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', 'no-change reconciliation requires an explicit semantic_no_change_reason and no semantic changes');
|
|
510
|
-
}
|
|
511
|
-
} else if (manifest.semantic_changes.length === 0) {
|
|
512
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', 'baseline updates require semantic change records');
|
|
513
|
-
}
|
|
514
|
-
const specRefs = new Map((baseline?.spec_refs || []).map(ref => [
|
|
515
|
-
ref.path,
|
|
516
|
-
ref
|
|
517
|
-
]));
|
|
518
|
-
const semanticPaths = new Set();
|
|
519
|
-
for (const item of manifest.semantic_changes) {
|
|
520
|
-
if (!item || typeof item !== 'object' || !CHANGE_ID.test(String(item.id || '')) || ![
|
|
521
|
-
'add',
|
|
522
|
-
'update'
|
|
523
|
-
].includes(item.action) || !/^[0-9a-f]{64}$/.test(String(item.after_digest || ''))) {
|
|
524
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', 'semantic change record is invalid');
|
|
525
|
-
}
|
|
526
|
-
let resolved;
|
|
527
|
-
try {
|
|
528
|
-
resolved = resolveProjectSourceRef(rootDir, item.source_ref, 'semantic_change.source_ref');
|
|
529
|
-
} catch (error) {
|
|
530
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', error.message);
|
|
531
|
-
}
|
|
532
|
-
semanticPaths.add(resolved.relative);
|
|
533
|
-
if (!updates.includes(resolved.relative) || resolved.digest !== item.after_digest) {
|
|
534
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', `semantic change digest or baseline update binding is invalid: ${ item.source_ref }`);
|
|
535
|
-
}
|
|
536
|
-
const before = specRefs.get(resolved.relative)?.digest || null;
|
|
537
|
-
if ((item.before_digest || null) !== before || item.action !== (before ? 'update' : 'add')) {
|
|
538
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', `semantic change before-state is invalid: ${ item.source_ref }`);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
const missingSemanticUpdates = updates.filter(update => !semanticPaths.has(update));
|
|
542
|
-
if (missingSemanticUpdates.length > 0) {
|
|
543
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', `baseline updates have no semantic record: ${ missingSemanticUpdates.join(', ') }`);
|
|
544
|
-
}
|
|
545
|
-
const gapIds = new Set((baseline?.gaps || []).map(gap => gap.id));
|
|
546
|
-
const unknowns = new Set((baseline?.unknowns || []).map(unknown => unknown.summary));
|
|
547
|
-
if (manifest.resolved_gap_ids.some(id => !gapIds.has(id)) || manifest.resolved_unknowns.some(summary => !unknowns.has(summary))) {
|
|
548
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', 'resolved gap or unknown does not exist in baseline authority');
|
|
549
|
-
}
|
|
550
|
-
return {
|
|
551
|
-
manifest,
|
|
552
|
-
relative,
|
|
553
|
-
file,
|
|
554
|
-
digest,
|
|
555
|
-
journal_intent: journalIntent
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
function readChange(db, id) {
|
|
559
|
-
return rowToChange(db.prepare('SELECT * FROM changes WHERE id = ?').get(id));
|
|
560
|
-
}
|
|
561
|
-
function listChanges(db, {status = null, kind = null, limit = 100} = {}) {
|
|
562
|
-
return db.prepare(LIST_CHANGES_SQL).all({
|
|
563
|
-
status,
|
|
564
|
-
kind,
|
|
565
|
-
maxn: Math.min(Math.max(limit, 1), 500)
|
|
566
|
-
}).map(rowToChange);
|
|
567
|
-
}
|
|
568
|
-
function writeIntent(file, change) {
|
|
569
|
-
const impact = change.docs_impact;
|
|
570
|
-
const recovery = change.contract.recovery;
|
|
571
|
-
const lines = [
|
|
572
|
-
`# ${ change.title }`,
|
|
573
|
-
'',
|
|
574
|
-
`- Change: \`${ change.id }\``,
|
|
575
|
-
`- Kind: \`${ change.kind }\``,
|
|
576
|
-
`- Base commit: \`${ change.base_commit || 'unavailable' }\``,
|
|
577
|
-
`- Documentation impact: \`${ impact.status }\``,
|
|
578
|
-
`- Research disposition: \`${ change.research_disposition.status }\``,
|
|
579
|
-
`- Alignment checkpoint: \`${ change.alignment_thread_id || 'not-required' }\``,
|
|
580
|
-
'',
|
|
581
|
-
'## Intent',
|
|
582
|
-
'',
|
|
583
|
-
change.intent,
|
|
584
|
-
'',
|
|
585
|
-
'## Outcome',
|
|
586
|
-
'',
|
|
587
|
-
change.contract.outcome,
|
|
588
|
-
'',
|
|
589
|
-
'## Acceptance',
|
|
590
|
-
'',
|
|
591
|
-
...change.contract.acceptance.flatMap(item => [
|
|
592
|
-
`### ${ item.id }`,
|
|
593
|
-
'',
|
|
594
|
-
item.criterion,
|
|
595
|
-
'',
|
|
596
|
-
`Verification: \`${ item.verification }\``,
|
|
597
|
-
''
|
|
598
|
-
]),
|
|
599
|
-
'## Public seams',
|
|
600
|
-
'',
|
|
601
|
-
...change.contract.public_seams.map(item => `- ${ item }`),
|
|
602
|
-
'',
|
|
603
|
-
'## Non-goals',
|
|
604
|
-
'',
|
|
605
|
-
...change.contract.non_goals.map(item => `- ${ item }`),
|
|
606
|
-
'',
|
|
607
|
-
'## Recovery',
|
|
608
|
-
'',
|
|
609
|
-
recovery?.strategy || 'Not yet decided.',
|
|
610
|
-
'',
|
|
611
|
-
`Verification: \`${ recovery?.verification || 'not yet decided' }\``,
|
|
612
|
-
'',
|
|
613
|
-
'## Classification',
|
|
614
|
-
'',
|
|
615
|
-
change.classification.rationale,
|
|
616
|
-
'',
|
|
617
|
-
`Risk flags: ${ change.classification.risk_flags.join(', ') || 'none' }`,
|
|
618
|
-
'',
|
|
619
|
-
'## Research routing',
|
|
620
|
-
'',
|
|
621
|
-
change.research_disposition.rationale,
|
|
622
|
-
''
|
|
623
|
-
];
|
|
624
|
-
if (impact.rationale)
|
|
625
|
-
lines.push('## Documentation rationale', '', impact.rationale, '');
|
|
626
|
-
if (change.baseline_bypass) {
|
|
627
|
-
lines.push('## Baseline break-glass', '', `- Approved by: ${ change.baseline_bypass.approved_by }`, `- Recorded at: ${ change.baseline_bypass.recorded_at }`, '', change.baseline_bypass.reason, '');
|
|
628
|
-
}
|
|
629
|
-
fs.writeFileSync(file, `${ lines.join('\n') }\n`);
|
|
630
|
-
}
|
|
631
|
-
function writeIncidentDiagnosis(file, change) {
|
|
632
|
-
const lines = [
|
|
633
|
-
`# Incident diagnosis: ${ change.title }`,
|
|
634
|
-
'',
|
|
635
|
-
...INCIDENT_DIAGNOSIS_SECTIONS.flatMap(({heading}) => [
|
|
636
|
-
`## ${ heading }`,
|
|
637
|
-
'',
|
|
638
|
-
''
|
|
639
|
-
])
|
|
640
|
-
];
|
|
641
|
-
fs.writeFileSync(file, `${ lines.join('\n') }\n`);
|
|
642
|
-
}
|
|
643
|
-
function upsertArtifact(db, {change_id, task_id = null, kind, artifactPath, contentHash, metadata, consumer_ref = null, terminal = false, writer = 'ultra.record', rootDir = process.cwd()}) {
|
|
644
|
-
const ownerType = task_id ? 'task' : 'change';
|
|
645
|
-
const ownerId = task_id || change_id;
|
|
646
|
-
const consumer = terminal ? null : consumer_ref || (task_id ? {
|
|
647
|
-
type: 'task',
|
|
648
|
-
id: task_id,
|
|
649
|
-
relation: 'consumed_by'
|
|
650
|
-
} : null);
|
|
651
|
-
if (!terminal && !consumer) {
|
|
652
|
-
throw new ChangeWorkflowError('ARTIFACT_CONSUMER_MISSING', 'artifact requires an explicit consumer');
|
|
653
|
-
}
|
|
654
|
-
return artifacts.recordArtifactInTx(db, {
|
|
655
|
-
owner_type: ownerType,
|
|
656
|
-
owner_id: ownerId,
|
|
657
|
-
kind,
|
|
658
|
-
path: artifactPath,
|
|
659
|
-
content_digest: contentHash || undefined,
|
|
660
|
-
source_refs: [{
|
|
661
|
-
type: 'change',
|
|
662
|
-
id: change_id,
|
|
663
|
-
relation: 'produced_for'
|
|
664
|
-
}],
|
|
665
|
-
consumer_refs: consumer ? [consumer] : [],
|
|
666
|
-
provenance: { writer },
|
|
667
|
-
metadata: {
|
|
668
|
-
...metadata || {},
|
|
669
|
-
...terminal ? { terminal_role: true } : {}
|
|
670
|
-
}
|
|
671
|
-
}, { rootDir });
|
|
672
|
-
}
|
|
673
|
-
function createChange(db, input, {rootDir = process.cwd()} = {}) {
|
|
674
|
-
assertExactFields(input, CHANGE_CREATE_FIELDS, 'change');
|
|
675
|
-
const id = typeof input?.id === 'string' ? input.id.trim() : '';
|
|
676
|
-
const title = typeof input?.title === 'string' ? input.title.trim() : '';
|
|
677
|
-
const intent = typeof input?.intent === 'string' ? input.intent.trim() : '';
|
|
678
|
-
if (!input || !CHANGE_ID.test(id) || title.length < 3 || intent.length < 3) {
|
|
679
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', 'id plus non-blank title and intent of at least three characters are required');
|
|
680
|
-
}
|
|
681
|
-
const kind = boundedVocabulary(input.kind, 'change.kind');
|
|
682
|
-
if (input.base_commit !== undefined && input.base_commit !== null && typeof input.base_commit !== 'string') {
|
|
683
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', 'base_commit must be a string or null');
|
|
684
|
-
}
|
|
685
|
-
if (input.supersedes_id !== undefined && input.supersedes_id !== null && (typeof input.supersedes_id !== 'string' || !CHANGE_ID.test(input.supersedes_id.trim()))) {
|
|
686
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', 'supersedes_id must be a valid Change id');
|
|
687
|
-
}
|
|
688
|
-
const baseCommit = typeof input.base_commit === 'string' ? input.base_commit.trim() : null;
|
|
689
|
-
if (input.baseline_bypass !== undefined) {
|
|
690
|
-
throw new ChangeWorkflowError(
|
|
691
|
-
'VALIDATION_ERROR',
|
|
692
|
-
'baseline_bypass is not part of the public Change draft contract',
|
|
693
|
-
);
|
|
694
|
-
}
|
|
695
|
-
if (readChange(db, id))
|
|
696
|
-
throw new ChangeWorkflowError('DUPLICATE_CHANGE_ID', `change ${ id } exists`);
|
|
697
|
-
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
698
|
-
const docsImpact = normalizeDocsImpact(input.docs_impact);
|
|
699
|
-
const providers = normalizeProviderRefs(input.provider_refs);
|
|
700
|
-
const contract = normalizeChangeContract(input.contract);
|
|
701
|
-
const classification = normalizeClassification(input.classification);
|
|
702
|
-
const researchDisposition = normalizeResearchDisposition(input.research_disposition);
|
|
703
|
-
const artifactRoot = path.join('.ultra', 'changes', 'active', id);
|
|
704
|
-
const artifactDir = path.resolve(rootDir, artifactRoot);
|
|
705
|
-
if (fs.existsSync(artifactDir)) {
|
|
706
|
-
throw new ChangeWorkflowError('CHANGE_ARTIFACT_EXISTS', `change artifact already exists: ${ artifactDir }`);
|
|
707
|
-
}
|
|
708
|
-
const ts = nowIso();
|
|
709
|
-
const row = {
|
|
710
|
-
id,
|
|
711
|
-
title,
|
|
712
|
-
kind,
|
|
713
|
-
status: 'active',
|
|
714
|
-
intent,
|
|
715
|
-
docs_impact: docsImpact,
|
|
716
|
-
provider_refs: providers,
|
|
717
|
-
baseline_bypass: null,
|
|
718
|
-
contract,
|
|
719
|
-
classification,
|
|
720
|
-
research_disposition: researchDisposition,
|
|
721
|
-
alignment_thread_id: null,
|
|
722
|
-
supersedes_id: input.supersedes_id?.trim() || null,
|
|
723
|
-
base_commit: baseCommit || gitHead(rootDir),
|
|
724
|
-
artifact_root: artifactRoot,
|
|
725
|
-
created_at: ts,
|
|
726
|
-
updated_at: ts
|
|
727
|
-
};
|
|
728
|
-
try {
|
|
729
|
-
return ops.tx(db, () => {
|
|
730
|
-
db.prepare(`INSERT INTO changes
|
|
731
|
-
(id, title, kind, status, intent, docs_impact_json, provider_refs_json,
|
|
732
|
-
baseline_bypass_json, contract_json, classification_json,
|
|
733
|
-
research_disposition_json, alignment_thread_id, base_commit, supersedes_id, artifact_root,
|
|
734
|
-
created_at, updated_at)
|
|
735
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(row.id, row.title, row.kind, row.status, row.intent, JSON.stringify(row.docs_impact), JSON.stringify(row.provider_refs), row.baseline_bypass ? JSON.stringify(row.baseline_bypass) : null, JSON.stringify(row.contract), JSON.stringify(row.classification), JSON.stringify(row.research_disposition), row.alignment_thread_id, row.base_commit, row.supersedes_id, row.artifact_root, ts, ts);
|
|
736
|
-
fs.mkdirSync(artifactDir, { recursive: true });
|
|
737
|
-
const intentPath = path.join(artifactDir, 'intent.md');
|
|
738
|
-
writeIntent(intentPath, row);
|
|
739
|
-
if (row.kind === 'incident') {
|
|
740
|
-
const diagnosisPath = path.join(artifactDir, 'diagnosis.md');
|
|
741
|
-
writeIncidentDiagnosis(diagnosisPath, row);
|
|
742
|
-
}
|
|
743
|
-
ops.appendEventInTx(db, {
|
|
744
|
-
type: 'change_created',
|
|
745
|
-
change_id: row.id,
|
|
746
|
-
payload: {
|
|
747
|
-
kind: row.kind,
|
|
748
|
-
artifact_root: row.artifact_root,
|
|
749
|
-
alignment_thread_id: row.alignment_thread_id,
|
|
750
|
-
supersedes_id: row.supersedes_id
|
|
751
|
-
}
|
|
752
|
-
});
|
|
753
|
-
upsertArtifact(db, {
|
|
754
|
-
change_id: row.id,
|
|
755
|
-
kind: 'intent',
|
|
756
|
-
artifactPath: path.relative(rootDir, intentPath),
|
|
757
|
-
contentHash: crypto.createHash('sha256').update(fs.readFileSync(intentPath)).digest('hex'),
|
|
758
|
-
consumer_ref: {
|
|
759
|
-
type: 'external',
|
|
760
|
-
id: 'ultra-plan',
|
|
761
|
-
relation: 'consumed_by'
|
|
762
|
-
},
|
|
763
|
-
writer: 'ultra.record',
|
|
764
|
-
rootDir
|
|
765
|
-
});
|
|
766
|
-
if (row.kind === 'incident') {
|
|
767
|
-
const diagnosisPath = path.join(artifactDir, 'diagnosis.md');
|
|
768
|
-
upsertArtifact(db, {
|
|
769
|
-
change_id: row.id,
|
|
770
|
-
kind: 'diagnosis',
|
|
771
|
-
artifactPath: path.relative(rootDir, diagnosisPath),
|
|
772
|
-
contentHash: crypto.createHash('sha256').update(fs.readFileSync(diagnosisPath)).digest('hex'),
|
|
773
|
-
metadata: { required_sections: INCIDENT_DIAGNOSIS_SECTIONS.map(({key}) => key) },
|
|
774
|
-
consumer_ref: {
|
|
775
|
-
type: 'external',
|
|
776
|
-
id: 'ultra-dev',
|
|
777
|
-
relation: 'consumed_by'
|
|
778
|
-
},
|
|
779
|
-
writer: 'ultra.record',
|
|
780
|
-
rootDir
|
|
781
|
-
});
|
|
782
|
-
}
|
|
783
|
-
return {
|
|
784
|
-
change: readChange(db, row.id),
|
|
785
|
-
intent_path: intentPath,
|
|
786
|
-
context_manifest_path: null,
|
|
787
|
-
workflow: null,
|
|
788
|
-
diagnostics: baselineHealth.status === 'pass' ? [] : (baselineHealth.blockers || []).map(code => ({
|
|
789
|
-
code,
|
|
790
|
-
severity: 'needs_attention'
|
|
791
|
-
}))
|
|
792
|
-
};
|
|
793
|
-
});
|
|
794
|
-
} catch (error) {
|
|
795
|
-
fs.rmSync(artifactDir, {
|
|
796
|
-
recursive: true,
|
|
797
|
-
force: true
|
|
798
|
-
});
|
|
799
|
-
throw error;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
function assertTaskCreationAllowed(db, input = {}, {
|
|
803
|
-
rootDir = process.cwd()
|
|
804
|
-
} = {}) {
|
|
805
|
-
const change = input.change_id ? readChange(db, input.change_id) : null;
|
|
806
|
-
if (input.change_id && !change) {
|
|
807
|
-
throw new ChangeWorkflowError('CHANGE_NOT_FOUND', 'change ' + input.change_id + ' not found');
|
|
808
|
-
}
|
|
809
|
-
if (change && ![
|
|
810
|
-
'active',
|
|
811
|
-
'blocked'
|
|
812
|
-
].includes(change.status)) {
|
|
813
|
-
throw new ChangeWorkflowError('CHANGE_NOT_MUTABLE', 'change ' + change.id + ' is ' + change.status);
|
|
814
|
-
}
|
|
815
|
-
const health = baselines.inspectBaseline(db, { rootDir });
|
|
816
|
-
return {
|
|
817
|
-
allowed: true,
|
|
818
|
-
diagnostics: health.status === 'pass' ? [] : (health.blockers || []).map(code => ({
|
|
819
|
-
code,
|
|
820
|
-
severity: 'needs_attention'
|
|
821
|
-
}))
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
function updateChange(db, id, patch = {}, {rootDir = process.cwd()} = {}) {
|
|
825
|
-
const normalizedId = typeof id === 'string' ? id.trim() : '';
|
|
826
|
-
if (!CHANGE_ID.test(normalizedId)) {
|
|
827
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', 'change id is invalid');
|
|
828
|
-
}
|
|
829
|
-
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
830
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', 'change patch must be an object');
|
|
831
|
-
}
|
|
832
|
-
const current = readChange(db, normalizedId);
|
|
833
|
-
if (!current) {
|
|
834
|
-
throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${ normalizedId } not found`);
|
|
835
|
-
}
|
|
836
|
-
if ([
|
|
837
|
-
'archived',
|
|
838
|
-
'cancelled'
|
|
839
|
-
].includes(current.status)) {
|
|
840
|
-
throw new ChangeWorkflowError('CHANGE_NOT_MUTABLE', `change ${ id } is ${ current.status }`);
|
|
841
|
-
}
|
|
842
|
-
for (const field of Object.keys(patch)) {
|
|
843
|
-
if (!CHANGE_PATCH_FIELDS.has(field)) {
|
|
844
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', `change field ${ field } is not patchable`);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
if (patch.status !== undefined && ![
|
|
848
|
-
'active',
|
|
849
|
-
'cancelled'
|
|
850
|
-
].includes(patch.status)) {
|
|
851
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', 'Change status may only be active or cancelled; readiness is derived from checkpoints');
|
|
852
|
-
}
|
|
853
|
-
const syncIntent = [
|
|
854
|
-
'title',
|
|
855
|
-
'intent',
|
|
856
|
-
'docs_impact',
|
|
857
|
-
'contract',
|
|
858
|
-
'classification',
|
|
859
|
-
'research_disposition'
|
|
860
|
-
].some(field => patch[field] !== undefined);
|
|
861
|
-
const intentPath = path.resolve(rootDir, current.artifact_root, 'intent.md');
|
|
862
|
-
const previousIntent = syncIntent && fs.existsSync(intentPath) ? fs.readFileSync(intentPath) : null;
|
|
863
|
-
try {
|
|
864
|
-
return ops.tx(db, () => {
|
|
865
|
-
const sets = [];
|
|
866
|
-
const values = [];
|
|
867
|
-
if (patch.title !== undefined) {
|
|
868
|
-
const title = typeof patch.title === 'string' ? patch.title.trim() : '';
|
|
869
|
-
if (!title)
|
|
870
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', 'change title cannot be empty');
|
|
871
|
-
sets.push('title = ?');
|
|
872
|
-
values.push(title);
|
|
873
|
-
}
|
|
874
|
-
if (patch.intent !== undefined) {
|
|
875
|
-
const intent = typeof patch.intent === 'string' ? patch.intent.trim() : '';
|
|
876
|
-
if (!intent)
|
|
877
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', 'change intent cannot be empty');
|
|
878
|
-
sets.push('intent = ?');
|
|
879
|
-
values.push(intent);
|
|
880
|
-
}
|
|
881
|
-
if (patch.docs_impact !== undefined) {
|
|
882
|
-
sets.push('docs_impact_json = ?');
|
|
883
|
-
values.push(JSON.stringify(normalizeDocsImpact(patch.docs_impact)));
|
|
884
|
-
}
|
|
885
|
-
if (patch.provider_refs !== undefined) {
|
|
886
|
-
sets.push('provider_refs_json = ?');
|
|
887
|
-
values.push(JSON.stringify(normalizeProviderRefs(patch.provider_refs)));
|
|
888
|
-
}
|
|
889
|
-
if (patch.contract !== undefined) {
|
|
890
|
-
sets.push('contract_json = ?');
|
|
891
|
-
values.push(JSON.stringify(normalizeChangeContract(patch.contract)));
|
|
892
|
-
}
|
|
893
|
-
if (patch.classification !== undefined) {
|
|
894
|
-
sets.push('classification_json = ?');
|
|
895
|
-
values.push(JSON.stringify(normalizeClassification(patch.classification)));
|
|
896
|
-
}
|
|
897
|
-
if (patch.research_disposition !== undefined) {
|
|
898
|
-
sets.push('research_disposition_json = ?');
|
|
899
|
-
values.push(JSON.stringify(normalizeResearchDisposition(patch.research_disposition)));
|
|
900
|
-
}
|
|
901
|
-
if (patch.status !== undefined) {
|
|
902
|
-
const allowed = new Set([
|
|
903
|
-
'active',
|
|
904
|
-
'cancelled'
|
|
905
|
-
]);
|
|
906
|
-
if (!allowed.has(patch.status)) {
|
|
907
|
-
throw new ChangeWorkflowError('ILLEGAL_CHANGE_TRANSITION', `cannot transition change ${ id } from ${ current.status } to ${ patch.status }`);
|
|
908
|
-
}
|
|
909
|
-
sets.push('status = ?');
|
|
910
|
-
values.push(patch.status);
|
|
911
|
-
} else if ([
|
|
912
|
-
'ready',
|
|
913
|
-
'blocked'
|
|
914
|
-
].includes(current.status) && Object.keys(patch).length > 0) {
|
|
915
|
-
sets.push('status = ?');
|
|
916
|
-
values.push('active');
|
|
917
|
-
}
|
|
918
|
-
if (sets.length === 0)
|
|
919
|
-
return current;
|
|
920
|
-
sets.push('updated_at = ?');
|
|
921
|
-
values.push(nowIso(), normalizedId);
|
|
922
|
-
db.prepare(`UPDATE changes SET ${ sets.join(', ') } WHERE id = ?`).run(...values);
|
|
923
|
-
const updated = readChange(db, normalizedId);
|
|
924
|
-
const invalidated = [];
|
|
925
|
-
let intentArtifactId = null;
|
|
926
|
-
if (syncIntent) {
|
|
927
|
-
fs.mkdirSync(path.dirname(intentPath), { recursive: true });
|
|
928
|
-
writeIntent(intentPath, updated);
|
|
929
|
-
const recorded = upsertArtifact(db, {
|
|
930
|
-
change_id: normalizedId,
|
|
931
|
-
kind: 'intent',
|
|
932
|
-
artifactPath: path.relative(rootDir, intentPath),
|
|
933
|
-
contentHash: crypto.createHash('sha256').update(fs.readFileSync(intentPath)).digest('hex'),
|
|
934
|
-
consumer_ref: {
|
|
935
|
-
type: 'external',
|
|
936
|
-
id: 'ultra-plan',
|
|
937
|
-
relation: 'consumed_by'
|
|
938
|
-
},
|
|
939
|
-
writer: 'ultra.record',
|
|
940
|
-
rootDir
|
|
941
|
-
});
|
|
942
|
-
intentArtifactId = recorded.artifact.id;
|
|
943
|
-
invalidated.push(...recorded.invalidated);
|
|
944
|
-
}
|
|
945
|
-
const semanticFields = Object.keys(patch).filter(field => SEMANTIC_AUTHORITY_FIELDS.has(field));
|
|
946
|
-
if (semanticFields.length > 0 && changeAuthority.changeStateDigest(current) !== changeAuthority.changeStateDigest(updated)) {
|
|
947
|
-
invalidated.push(...artifacts.invalidateConsumersFromEndpointInTx(db, {
|
|
948
|
-
type: 'change',
|
|
949
|
-
id: normalizedId
|
|
950
|
-
}, {
|
|
951
|
-
reason: 'change_semantic_authority_updated',
|
|
952
|
-
exclude: intentArtifactId ? [{
|
|
953
|
-
type: 'artifact',
|
|
954
|
-
id: intentArtifactId
|
|
955
|
-
}] : []
|
|
956
|
-
}));
|
|
957
|
-
}
|
|
958
|
-
const invalidatedTasks = [...new Set(invalidated.filter(item => item.type === 'task').map(item => item.id))].sort();
|
|
959
|
-
ops.appendEventInTx(db, {
|
|
960
|
-
type: 'change_updated',
|
|
961
|
-
change_id: normalizedId,
|
|
962
|
-
payload: {
|
|
963
|
-
fields: Object.keys(patch),
|
|
964
|
-
invalidated_tasks: invalidatedTasks,
|
|
965
|
-
invalidated_authorities: invalidated
|
|
966
|
-
}
|
|
967
|
-
});
|
|
968
|
-
return updated;
|
|
969
|
-
});
|
|
970
|
-
} catch (error) {
|
|
971
|
-
if (syncIntent) {
|
|
972
|
-
if (previousIntent === null)
|
|
973
|
-
fs.rmSync(intentPath, { force: true });
|
|
974
|
-
else
|
|
975
|
-
fs.writeFileSync(intentPath, previousIntent);
|
|
976
|
-
}
|
|
977
|
-
throw error;
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
function recordDelta(db, input, {
|
|
981
|
-
rootDir = process.cwd()
|
|
982
|
-
} = {}) {
|
|
983
|
-
const change = readChange(db, input?.id);
|
|
984
|
-
if (!change) {
|
|
985
|
-
throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${ input?.id || '(missing)' } not found`);
|
|
986
|
-
}
|
|
987
|
-
return changePacket.recordDelta(db, change, input, { rootDir });
|
|
988
|
-
}
|
|
989
|
-
function recordDocumentationReconciliation(db, input, {
|
|
990
|
-
rootDir = process.cwd()
|
|
991
|
-
} = {}) {
|
|
992
|
-
const change = readChange(db, input?.id);
|
|
993
|
-
if (!change) {
|
|
994
|
-
throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${ input?.id || '(missing)' } not found`);
|
|
995
|
-
}
|
|
996
|
-
return changePacket.recordDocumentationReconciliation(db, change, input, { rootDir });
|
|
997
|
-
}
|
|
998
|
-
function markdownAnchor(text, fallback) {
|
|
999
|
-
const heading = String(text).match(/^#{1,6}\s+(.+?)\s*#*\s*$/m)?.[1] || fallback;
|
|
1000
|
-
return String(heading || 'change').trim().toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, '').replace(/\s+/g, '-').replace(/-+/g, '-');
|
|
1001
|
-
}
|
|
1002
|
-
function generatePacketReconciliation(db, change, packet, rootDir) {
|
|
1003
|
-
const baseline = baselines.readBaseline(db);
|
|
1004
|
-
const semanticChanges = packet.delta.value.mutations.map(mutation => {
|
|
1005
|
-
const overlay = safeRelativePath(rootDir, mutation.overlay_path);
|
|
1006
|
-
const anchor = markdownAnchor(fs.readFileSync(overlay, 'utf8'), path.basename(mutation.target_path, path.extname(mutation.target_path)));
|
|
1007
|
-
return {
|
|
1008
|
-
id: `delta-${ mutation.id }`,
|
|
1009
|
-
action: mutation.action,
|
|
1010
|
-
source_ref: `${ mutation.target_path }#${ anchor }`,
|
|
1011
|
-
before_digest: mutation.before_digest,
|
|
1012
|
-
after_digest: mutation.after_digest
|
|
1013
|
-
};
|
|
1014
|
-
});
|
|
1015
|
-
const verification = [{
|
|
1016
|
-
name: 'typed Change packet read-back',
|
|
1017
|
-
command: 'change.delta',
|
|
1018
|
-
status: 'pass',
|
|
1019
|
-
evidence: `Change delta ${ packet.delta.artifact.id } @ ${ packet.delta.artifact.digest } and documentation reconciliation ${ packet.documentation?.artifact.id || 'none-required' } were read back before Deliver.`
|
|
1020
|
-
}];
|
|
1021
|
-
const manifest = {
|
|
1022
|
-
$schema: 'ultra-baseline-reconciliation-v1',
|
|
1023
|
-
change_id: change.id,
|
|
1024
|
-
baseline_id: baseline?.id || null,
|
|
1025
|
-
baseline_updates: packet.baseline_updates,
|
|
1026
|
-
semantic_changes: semanticChanges,
|
|
1027
|
-
resolved_gap_ids: [],
|
|
1028
|
-
resolved_unknowns: [],
|
|
1029
|
-
verification,
|
|
1030
|
-
...semanticChanges.length === 0 ? { semantic_no_change_reason: packet.delta.value.no_semantic_change_reason || 'The accepted Change has no baseline specification mutation.' } : {}
|
|
1031
|
-
};
|
|
1032
|
-
const relative = `${ change.artifact_root }/baseline-reconciliation.json`;
|
|
1033
|
-
const file = safeRelativePath(rootDir, relative);
|
|
1034
|
-
const previous = fs.existsSync(file) ? fs.readFileSync(file) : null;
|
|
1035
|
-
const serialized = `${ JSON.stringify(manifest, null, 2) }\n`;
|
|
1036
|
-
fs.writeFileSync(file, serialized);
|
|
1037
|
-
try {
|
|
1038
|
-
const recorded = artifacts.recordArtifact(db, {
|
|
1039
|
-
id: `${ change.id }-baseline-reconciliation`,
|
|
1040
|
-
owner_type: 'change',
|
|
1041
|
-
owner_id: change.id,
|
|
1042
|
-
kind: 'baseline_reconciliation',
|
|
1043
|
-
path: relative,
|
|
1044
|
-
content_digest: crypto.createHash('sha256').update(serialized).digest('hex'),
|
|
1045
|
-
source_refs: [
|
|
1046
|
-
{
|
|
1047
|
-
type: 'artifact',
|
|
1048
|
-
id: packet.delta.artifact.id,
|
|
1049
|
-
relation: 'reconciles'
|
|
1050
|
-
},
|
|
1051
|
-
...packet.documentation ? [{
|
|
1052
|
-
type: 'artifact',
|
|
1053
|
-
id: packet.documentation.artifact.id,
|
|
1054
|
-
relation: 'reconciles_documentation'
|
|
1055
|
-
}] : []
|
|
1056
|
-
],
|
|
1057
|
-
consumer_refs: [],
|
|
1058
|
-
provenance: {
|
|
1059
|
-
writer: 'change.deliver',
|
|
1060
|
-
generated: true
|
|
1061
|
-
},
|
|
1062
|
-
metadata: {
|
|
1063
|
-
terminal_role: true,
|
|
1064
|
-
schema: manifest.$schema
|
|
1065
|
-
}
|
|
1066
|
-
}, { rootDir }).artifact;
|
|
1067
|
-
return {
|
|
1068
|
-
manifest,
|
|
1069
|
-
relative,
|
|
1070
|
-
file,
|
|
1071
|
-
digest: recorded.digest,
|
|
1072
|
-
artifact: recorded
|
|
1073
|
-
};
|
|
1074
|
-
} catch (error) {
|
|
1075
|
-
if (previous === null)
|
|
1076
|
-
fs.rmSync(file, { force: true });
|
|
1077
|
-
else
|
|
1078
|
-
fs.writeFileSync(file, previous);
|
|
1079
|
-
throw error;
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
function generateCallerNoChangeReconciliation(db, change, input, rootDir) {
|
|
1083
|
-
const baseline = baselines.readBaseline(db);
|
|
1084
|
-
const manifest = {
|
|
1085
|
-
$schema: 'ultra-baseline-reconciliation-v1',
|
|
1086
|
-
change_id: change.id,
|
|
1087
|
-
baseline_id: baseline?.id || null,
|
|
1088
|
-
baseline_updates: [],
|
|
1089
|
-
semantic_changes: [],
|
|
1090
|
-
semantic_no_change_reason: String(input.no_baseline_change_reason).trim(),
|
|
1091
|
-
resolved_gap_ids: [],
|
|
1092
|
-
resolved_unknowns: [],
|
|
1093
|
-
verification: [{
|
|
1094
|
-
name: 'explicit archive handoff',
|
|
1095
|
-
command: 'ultra.archive',
|
|
1096
|
-
status: 'pass',
|
|
1097
|
-
evidence: String(input.summary).trim()
|
|
1098
|
-
}]
|
|
1099
|
-
};
|
|
1100
|
-
const relative = `${ change.artifact_root }/baseline-reconciliation.json`;
|
|
1101
|
-
const serialized = `${ JSON.stringify(manifest, null, 2) }\n`;
|
|
1102
|
-
const file = safeRelativePath(rootDir, relative);
|
|
1103
|
-
const previous = fs.existsSync(file) ? fs.readFileSync(file) : null;
|
|
1104
|
-
const published = writeManagedFile(rootDir, relative, Buffer.from(serialized));
|
|
1105
|
-
try {
|
|
1106
|
-
const recorded = artifacts.recordArtifact(db, {
|
|
1107
|
-
id: `${ change.id }-baseline-reconciliation`,
|
|
1108
|
-
owner_type: 'change',
|
|
1109
|
-
owner_id: change.id,
|
|
1110
|
-
kind: 'baseline_reconciliation',
|
|
1111
|
-
path: relative,
|
|
1112
|
-
content_digest: published.digest,
|
|
1113
|
-
source_refs: [],
|
|
1114
|
-
consumer_refs: [],
|
|
1115
|
-
provenance: {
|
|
1116
|
-
writer: 'ultra.archive',
|
|
1117
|
-
generated: true
|
|
1118
|
-
},
|
|
1119
|
-
metadata: {
|
|
1120
|
-
terminal_role: true,
|
|
1121
|
-
schema: manifest.$schema,
|
|
1122
|
-
caller_declared: true
|
|
1123
|
-
}
|
|
1124
|
-
}, { rootDir }).artifact;
|
|
1125
|
-
return {
|
|
1126
|
-
manifest,
|
|
1127
|
-
relative,
|
|
1128
|
-
file,
|
|
1129
|
-
digest: recorded.digest,
|
|
1130
|
-
artifact: recorded
|
|
1131
|
-
};
|
|
1132
|
-
} catch (error) {
|
|
1133
|
-
if (previous === null)
|
|
1134
|
-
fs.rmSync(file, { force: true });
|
|
1135
|
-
else
|
|
1136
|
-
fs.writeFileSync(file, previous);
|
|
1137
|
-
throw error;
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
function replaceArchiveAuthorityText(text, source, destination, digestMap = new Map(), {
|
|
1141
|
-
normalizePath = false
|
|
1142
|
-
} = {}) {
|
|
1143
|
-
const original = String(text);
|
|
1144
|
-
const windowsSource = source.replaceAll('/', '\\');
|
|
1145
|
-
let result = original.replaceAll(source, destination).replaceAll(windowsSource, destination);
|
|
1146
|
-
if (normalizePath && original.includes(windowsSource)) {
|
|
1147
|
-
result = result.replaceAll('\\', '/');
|
|
1148
|
-
}
|
|
1149
|
-
for (const [before, after] of digestMap) {
|
|
1150
|
-
if (before !== after)
|
|
1151
|
-
result = result.replaceAll(before, after);
|
|
1152
|
-
}
|
|
1153
|
-
return result;
|
|
1154
|
-
}
|
|
1155
|
-
function kernelArchiveContextPath(change, id) {
|
|
1156
|
-
return `${ change.artifact_root }/authority/context-envelopes/${ id }.json`;
|
|
1157
|
-
}
|
|
1158
|
-
function kernelArchiveWorkerPath(change, id) {
|
|
1159
|
-
return `${ change.artifact_root }/authority/worker-packets/${ id }.json`;
|
|
1160
|
-
}
|
|
1161
|
-
function materializeKernelArchiveAuthority(db, change, rootDir) {
|
|
1162
|
-
const taskIds = db.prepare('SELECT id FROM tasks WHERE change_id = ? ORDER BY id').all(change.id).map(row => row.id);
|
|
1163
|
-
const contexts = db.prepare(`SELECT * FROM context_envelopes
|
|
1164
|
-
WHERE (scope_type = 'change' AND scope_id = ?)
|
|
1165
|
-
OR (scope_type = 'task' AND scope_id IN (
|
|
1166
|
-
SELECT id FROM tasks WHERE change_id = ?
|
|
1167
|
-
))
|
|
1168
|
-
ORDER BY id`).all(change.id, change.id);
|
|
1169
|
-
for (const row of contexts) {
|
|
1170
|
-
const read = readStableProjectFile(rootDir, row.artifact_path);
|
|
1171
|
-
if (!row.file_digest || read.digest !== row.file_digest) {
|
|
1172
|
-
throw new ChangeWorkflowError('CONTEXT_ENVELOPE_FILE_DRIFT', `cannot archive unbound Context Envelope ${ row.id }`, {
|
|
1173
|
-
path: row.artifact_path,
|
|
1174
|
-
expected: row.file_digest || null,
|
|
1175
|
-
actual: read.digest
|
|
1176
|
-
});
|
|
1177
|
-
}
|
|
1178
|
-
const target = kernelArchiveContextPath(change, row.id);
|
|
1179
|
-
const published = writeManagedFile(rootDir, target, read.bytes);
|
|
1180
|
-
upsertArtifact(db, {
|
|
1181
|
-
change_id: change.id,
|
|
1182
|
-
task_id: row.scope_type === 'task' ? row.scope_id : null,
|
|
1183
|
-
kind: 'archive_context_envelope',
|
|
1184
|
-
artifactPath: target,
|
|
1185
|
-
contentHash: published.digest,
|
|
1186
|
-
consumer_ref: {
|
|
1187
|
-
type: 'external',
|
|
1188
|
-
id: 'ultra.archive',
|
|
1189
|
-
relation: 'archived_by'
|
|
1190
|
-
},
|
|
1191
|
-
writer: 'ultra.archive',
|
|
1192
|
-
rootDir
|
|
1193
|
-
});
|
|
1194
|
-
}
|
|
1195
|
-
if (taskIds.length === 0)
|
|
1196
|
-
return;
|
|
1197
|
-
const packets = db.prepare(`SELECT * FROM worker_packets
|
|
1198
|
-
WHERE scope_type = 'task' AND status = 'assigned'
|
|
1199
|
-
AND scope_id IN (SELECT id FROM tasks WHERE change_id = ?)
|
|
1200
|
-
ORDER BY id`).all(change.id);
|
|
1201
|
-
for (const row of packets) {
|
|
1202
|
-
const read = readStableProjectFile(rootDir, row.packet_path);
|
|
1203
|
-
if (!row.file_digest || read.digest !== row.file_digest) {
|
|
1204
|
-
throw new ChangeWorkflowError('WORKER_PACKET_FILE_DRIFT', `cannot archive unbound Worker Packet ${ row.id }`, {
|
|
1205
|
-
path: row.packet_path,
|
|
1206
|
-
expected: row.file_digest || null,
|
|
1207
|
-
actual: read.digest
|
|
1208
|
-
});
|
|
1209
|
-
}
|
|
1210
|
-
const target = kernelArchiveWorkerPath(change, row.id);
|
|
1211
|
-
const published = writeManagedFile(rootDir, target, read.bytes);
|
|
1212
|
-
upsertArtifact(db, {
|
|
1213
|
-
change_id: change.id,
|
|
1214
|
-
task_id: row.scope_id,
|
|
1215
|
-
kind: 'archive_worker_packet',
|
|
1216
|
-
artifactPath: target,
|
|
1217
|
-
contentHash: published.digest,
|
|
1218
|
-
consumer_ref: {
|
|
1219
|
-
type: 'external',
|
|
1220
|
-
id: 'ultra.archive',
|
|
1221
|
-
relation: 'archived_by'
|
|
1222
|
-
},
|
|
1223
|
-
writer: 'ultra.archive',
|
|
1224
|
-
rootDir
|
|
1225
|
-
});
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
function normalizedArchiveDocument(bytes, intent) {
|
|
1229
|
-
const text = bytes.toString('utf8');
|
|
1230
|
-
if (!Buffer.from(text, 'utf8').equals(bytes))
|
|
1231
|
-
return {
|
|
1232
|
-
bytes,
|
|
1233
|
-
semanticDigest: null
|
|
1234
|
-
};
|
|
1235
|
-
let document;
|
|
1236
|
-
try {
|
|
1237
|
-
document = JSON.parse(text);
|
|
1238
|
-
} catch {
|
|
1239
|
-
return {
|
|
1240
|
-
bytes,
|
|
1241
|
-
semanticDigest: null
|
|
1242
|
-
};
|
|
1243
|
-
}
|
|
1244
|
-
let semanticDigest = null;
|
|
1245
|
-
if (document?.schema_version === '1.0' && document.detail === 'full' && document.envelope && document.digest) {
|
|
1246
|
-
if (document.envelope.change?.id === intent.change_id) {
|
|
1247
|
-
document.envelope.change.status = 'archived';
|
|
1248
|
-
}
|
|
1249
|
-
semanticDigest = canonical.digest(document.envelope);
|
|
1250
|
-
document.digest = semanticDigest;
|
|
1251
|
-
} else if (document?.packet_version && document.packet_digest) {
|
|
1252
|
-
const {
|
|
1253
|
-
packet_digest: _prior,
|
|
1254
|
-
...value
|
|
1255
|
-
} = document;
|
|
1256
|
-
semanticDigest = canonical.digest(value);
|
|
1257
|
-
document.packet_digest = semanticDigest;
|
|
1258
|
-
} else if (document?.schema_version === '1.0' && document.question && document.selection && document.digest) {
|
|
1259
|
-
const {
|
|
1260
|
-
digest: _prior,
|
|
1261
|
-
...value
|
|
1262
|
-
} = document;
|
|
1263
|
-
semanticDigest = canonical.digest(value);
|
|
1264
|
-
document.digest = semanticDigest;
|
|
1265
|
-
} else if (document?.change?.id === intent.change_id) {
|
|
1266
|
-
document.change.status = 'archived';
|
|
1267
|
-
}
|
|
1268
|
-
return {
|
|
1269
|
-
bytes: Buffer.from(`${ JSON.stringify(document, null, 2) }\n`),
|
|
1270
|
-
semanticDigest
|
|
1271
|
-
};
|
|
1272
|
-
}
|
|
1273
|
-
function archiveRebindAuthorityValue(value, source, destination, digestMap) {
|
|
1274
|
-
if (typeof value === 'string') {
|
|
1275
|
-
return replaceArchiveAuthorityText(value, source, destination, digestMap, { normalizePath: true });
|
|
1276
|
-
}
|
|
1277
|
-
if (Array.isArray(value)) {
|
|
1278
|
-
return value.map(item => archiveRebindAuthorityValue(item, source, destination, digestMap));
|
|
1279
|
-
}
|
|
1280
|
-
if (value && typeof value === 'object') {
|
|
1281
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
1282
|
-
key,
|
|
1283
|
-
archiveRebindAuthorityValue(item, source, destination, digestMap)
|
|
1284
|
-
]));
|
|
1285
|
-
}
|
|
1286
|
-
return value;
|
|
1287
|
-
}
|
|
1288
|
-
function buildArchiveRebindEntries(db, intent, rootDir) {
|
|
1289
|
-
const tree = walkStableProjectTree(rootDir, intent.destination, { ignore: relative => relative === `${ intent.destination }/${ archiveJournal.INTENT_FILE }` || relative === `${ intent.destination }/${ archiveJournal.REBIND_FILE }` });
|
|
1290
|
-
if (tree.unsafe.length > 0) {
|
|
1291
|
-
throw new ChangeWorkflowError('ARCHIVE_PATH_UNSAFE', `archive packet contains unsafe entries: ${ tree.unsafe.map(item => item.path).join(', ') }`, { unsafe: tree.unsafe });
|
|
1292
|
-
}
|
|
1293
|
-
const originalByPath = new Map();
|
|
1294
|
-
const semanticByPath = new Map();
|
|
1295
|
-
for (const artifact of db.prepare('SELECT path, digest, content_hash FROM artifacts WHERE change_id = ?').all(intent.change_id)) {
|
|
1296
|
-
const archivedPath = rebindRegistryPath(artifact.path, intent.source, intent.destination);
|
|
1297
|
-
const digest = artifact.digest || artifact.content_hash;
|
|
1298
|
-
if (archivedPath && digest)
|
|
1299
|
-
originalByPath.set(archivedPath, digest);
|
|
1300
|
-
}
|
|
1301
|
-
const contextPaths = new Set();
|
|
1302
|
-
for (const snapshot of db.prepare('SELECT manifest_path, manifest_hash FROM context_snapshots WHERE change_id = ?').all(intent.change_id)) {
|
|
1303
|
-
const archivedPath = rebindRegistryPath(snapshot.manifest_path, intent.source, intent.destination);
|
|
1304
|
-
if (archivedPath) {
|
|
1305
|
-
contextPaths.add(archivedPath);
|
|
1306
|
-
originalByPath.set(archivedPath, snapshot.manifest_hash);
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
for (const row of db.prepare(`SELECT id, digest FROM decision_records
|
|
1310
|
-
WHERE scope_type = 'change' AND scope_id = ?`).all(intent.change_id)) {
|
|
1311
|
-
const archivedPath = `${ intent.destination }/decisions/${ row.id }.json`;
|
|
1312
|
-
semanticByPath.set(archivedPath, row.digest);
|
|
1313
|
-
}
|
|
1314
|
-
for (const row of db.prepare(`SELECT id, digest FROM context_envelopes
|
|
1315
|
-
WHERE (scope_type = 'change' AND scope_id = ?)
|
|
1316
|
-
OR (scope_type = 'task' AND scope_id IN (
|
|
1317
|
-
SELECT id FROM tasks WHERE change_id = ?
|
|
1318
|
-
))`).all(intent.change_id, intent.change_id)) {
|
|
1319
|
-
semanticByPath.set(`${ intent.destination }/authority/context-envelopes/${ row.id }.json`, row.digest);
|
|
1320
|
-
}
|
|
1321
|
-
for (const row of db.prepare(`SELECT id, packet_digest FROM worker_packets
|
|
1322
|
-
WHERE scope_type = 'task' AND status = 'assigned'
|
|
1323
|
-
AND scope_id IN (SELECT id FROM tasks WHERE change_id = ?)`).all(intent.change_id)) {
|
|
1324
|
-
semanticByPath.set(`${ intent.destination }/authority/worker-packets/${ row.id }.json`, row.packet_digest);
|
|
1325
|
-
}
|
|
1326
|
-
const original = new Map();
|
|
1327
|
-
const pathRebound = new Map();
|
|
1328
|
-
for (const relative of tree.files) {
|
|
1329
|
-
const read = readStableProjectFile(rootDir, relative);
|
|
1330
|
-
original.set(relative, read.bytes);
|
|
1331
|
-
const text = read.bytes.toString('utf8');
|
|
1332
|
-
if (!Buffer.from(text, 'utf8').equals(read.bytes)) {
|
|
1333
|
-
if (read.bytes.includes(Buffer.from(intent.source))) {
|
|
1334
|
-
throw new ChangeWorkflowError('ARCHIVE_REBIND_UNSUPPORTED', `archive packet contains a binary active-root reference: ${ relative }`);
|
|
1335
|
-
}
|
|
1336
|
-
pathRebound.set(relative, read.bytes);
|
|
1337
|
-
continue;
|
|
1338
|
-
}
|
|
1339
|
-
let next = replaceArchiveAuthorityText(text, intent.source, intent.destination);
|
|
1340
|
-
if (contextPaths.has(relative)) {
|
|
1341
|
-
let manifest;
|
|
1342
|
-
try {
|
|
1343
|
-
manifest = JSON.parse(next);
|
|
1344
|
-
} catch (error) {
|
|
1345
|
-
throw new ChangeWorkflowError('ARCHIVE_CONTEXT_INVALID', `archived Context Manifest cannot be parsed: ${ relative }: ${ error.message }`);
|
|
1346
|
-
}
|
|
1347
|
-
if (manifest.change?.id !== intent.change_id) {
|
|
1348
|
-
throw new ChangeWorkflowError('ARCHIVE_CONTEXT_INVALID', `archived Context Manifest belongs to another change: ${ relative }`);
|
|
1349
|
-
}
|
|
1350
|
-
manifest.change.status = 'archived';
|
|
1351
|
-
next = `${ JSON.stringify(manifest, null, 2) }\n`;
|
|
1352
|
-
}
|
|
1353
|
-
pathRebound.set(relative, normalizedArchiveDocument(Buffer.from(next), intent).bytes);
|
|
1354
|
-
}
|
|
1355
|
-
let current = new Map(pathRebound);
|
|
1356
|
-
for (let iteration = 0; iteration < 20; iteration += 1) {
|
|
1357
|
-
const digestMap = new Map();
|
|
1358
|
-
for (const [relative, beforeDigest] of originalByPath) {
|
|
1359
|
-
const bytes = current.get(relative);
|
|
1360
|
-
if (bytes) {
|
|
1361
|
-
digestMap.set(beforeDigest, crypto.createHash('sha256').update(bytes).digest('hex'));
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
for (const [relative, beforeDigest] of semanticByPath) {
|
|
1365
|
-
const bytes = current.get(relative);
|
|
1366
|
-
if (!bytes)
|
|
1367
|
-
continue;
|
|
1368
|
-
const normalized = normalizedArchiveDocument(bytes, intent);
|
|
1369
|
-
if (normalized.semanticDigest) {
|
|
1370
|
-
digestMap.set(beforeDigest, normalized.semanticDigest);
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
const next = new Map();
|
|
1374
|
-
let changed = false;
|
|
1375
|
-
for (const [relative, baseBytes] of pathRebound) {
|
|
1376
|
-
const text = baseBytes.toString('utf8');
|
|
1377
|
-
if (!Buffer.from(text, 'utf8').equals(baseBytes)) {
|
|
1378
|
-
next.set(relative, baseBytes);
|
|
1379
|
-
continue;
|
|
1380
|
-
}
|
|
1381
|
-
const replaced = Buffer.from(replaceArchiveAuthorityText(text, intent.source, intent.destination, digestMap));
|
|
1382
|
-
const bytes = normalizedArchiveDocument(replaced, intent).bytes;
|
|
1383
|
-
next.set(relative, bytes);
|
|
1384
|
-
if (!bytes.equals(current.get(relative)))
|
|
1385
|
-
changed = true;
|
|
1386
|
-
}
|
|
1387
|
-
current = next;
|
|
1388
|
-
if (!changed)
|
|
1389
|
-
break;
|
|
1390
|
-
if (iteration === 19) {
|
|
1391
|
-
throw new ChangeWorkflowError('ARCHIVE_REBIND_CYCLE', 'archive packet digest references did not converge');
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
return [...original.entries()].flatMap(([relative, before]) => {
|
|
1395
|
-
const after = current.get(relative);
|
|
1396
|
-
if (after.equals(before))
|
|
1397
|
-
return [];
|
|
1398
|
-
return [{
|
|
1399
|
-
relative_path: path.posix.relative(intent.destination, relative),
|
|
1400
|
-
before_digest: crypto.createHash('sha256').update(before).digest('hex'),
|
|
1401
|
-
after_digest: crypto.createHash('sha256').update(after).digest('hex'),
|
|
1402
|
-
before_base64: before.toString('base64'),
|
|
1403
|
-
after_base64: after.toString('base64')
|
|
1404
|
-
}];
|
|
1405
|
-
}).sort((left, right) => left.relative_path.localeCompare(right.relative_path));
|
|
1406
|
-
}
|
|
1407
|
-
function archiveRebindState(db, intent, rootDir) {
|
|
1408
|
-
const existing = archiveJournal.readArchiveRebind(rootDir, intent);
|
|
1409
|
-
const entries = existing?.entries || buildArchiveRebindEntries(db, intent, rootDir);
|
|
1410
|
-
const journal = archiveJournal.prepareArchiveRebind(rootDir, intent, existing ? null : entries);
|
|
1411
|
-
const byPath = new Map(journal.entries.map(entry => [
|
|
1412
|
-
`${ intent.destination }/${ entry.relative_path }`,
|
|
1413
|
-
entry
|
|
1414
|
-
]));
|
|
1415
|
-
const digestMap = new Map(journal.entries.map(entry => [
|
|
1416
|
-
entry.before_digest,
|
|
1417
|
-
entry.after_digest
|
|
1418
|
-
]));
|
|
1419
|
-
const semanticEntry = relativePath => byPath.get(relativePath);
|
|
1420
|
-
for (const row of db.prepare(`SELECT id, digest FROM decision_records
|
|
1421
|
-
WHERE scope_type = 'change' AND scope_id = ?`).all(intent.change_id)) {
|
|
1422
|
-
const entry = semanticEntry(`${ intent.destination }/decisions/${ row.id }.json`);
|
|
1423
|
-
if (!entry)
|
|
1424
|
-
continue;
|
|
1425
|
-
const document = JSON.parse(Buffer.from(entry.after_base64, 'base64').toString('utf8'));
|
|
1426
|
-
if (document.digest)
|
|
1427
|
-
digestMap.set(row.digest, document.digest);
|
|
1428
|
-
}
|
|
1429
|
-
for (const row of db.prepare(`SELECT id, digest FROM context_envelopes
|
|
1430
|
-
WHERE (scope_type = 'change' AND scope_id = ?)
|
|
1431
|
-
OR (scope_type = 'task' AND scope_id IN (
|
|
1432
|
-
SELECT id FROM tasks WHERE change_id = ?
|
|
1433
|
-
))`).all(intent.change_id, intent.change_id)) {
|
|
1434
|
-
const entry = semanticEntry(`${ intent.destination }/authority/context-envelopes/${ row.id }.json`);
|
|
1435
|
-
if (!entry)
|
|
1436
|
-
continue;
|
|
1437
|
-
const document = JSON.parse(Buffer.from(entry.after_base64, 'base64').toString('utf8'));
|
|
1438
|
-
if (document.digest)
|
|
1439
|
-
digestMap.set(row.digest, document.digest);
|
|
1440
|
-
}
|
|
1441
|
-
for (const row of db.prepare(`SELECT id, packet_digest FROM worker_packets
|
|
1442
|
-
WHERE scope_type = 'task' AND status = 'assigned'
|
|
1443
|
-
AND scope_id IN (SELECT id FROM tasks WHERE change_id = ?)`).all(intent.change_id)) {
|
|
1444
|
-
const entry = semanticEntry(`${ intent.destination }/authority/worker-packets/${ row.id }.json`);
|
|
1445
|
-
if (!entry)
|
|
1446
|
-
continue;
|
|
1447
|
-
const document = JSON.parse(Buffer.from(entry.after_base64, 'base64').toString('utf8'));
|
|
1448
|
-
if (document.packet_digest) {
|
|
1449
|
-
digestMap.set(row.packet_digest, document.packet_digest);
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
return {
|
|
1453
|
-
journal,
|
|
1454
|
-
byPath,
|
|
1455
|
-
digestMap
|
|
1456
|
-
};
|
|
1457
|
-
}
|
|
1458
|
-
function finalizeArchive(db, intent, {rootDir, journalIntent = intent}) {
|
|
1459
|
-
const change = readChange(db, intent.change_id);
|
|
1460
|
-
if (!change)
|
|
1461
|
-
throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${ intent.change_id } not found`);
|
|
1462
|
-
try {
|
|
1463
|
-
archiveJournal.verifyArchiveIntent(rootDir, journalIntent, { location: 'destination' });
|
|
1464
|
-
} catch (error) {
|
|
1465
|
-
throw new ChangeWorkflowError('ARCHIVE_PATH_UNSAFE', `archive transition cannot trust its physical destination: ${ error.message }`, { cause: error.code || error.message });
|
|
1466
|
-
}
|
|
1467
|
-
const relative = artifacts.normalizeRelativePath(intent.destination);
|
|
1468
|
-
const destination = safeRelativePath(rootDir, relative);
|
|
1469
|
-
if (change.status === 'archived' && artifacts.normalizeRelativePath(change.artifact_root) === relative) {
|
|
1470
|
-
return {
|
|
1471
|
-
change,
|
|
1472
|
-
archive_path: destination
|
|
1473
|
-
};
|
|
1474
|
-
}
|
|
1475
|
-
const acceptedStatuses = [
|
|
1476
|
-
'active',
|
|
1477
|
-
'ready'
|
|
1478
|
-
];
|
|
1479
|
-
if (!acceptedStatuses.includes(change.status)) {
|
|
1480
|
-
throw new ChangeWorkflowError('CHANGE_NOT_READY', `change ${ change.id } must have complete accepted checkpoints to finish archive`);
|
|
1481
|
-
}
|
|
1482
|
-
const rebind = archiveRebindState(db, journalIntent, rootDir);
|
|
1483
|
-
const archivedReconciliationPath = rebindRegistryPath(intent.reconciliation_path, intent.source, relative);
|
|
1484
|
-
const reconciliationRead = readStableProjectFile(rootDir, archivedReconciliationPath, { encoding: 'utf8' });
|
|
1485
|
-
let reconciliationManifest;
|
|
1486
|
-
try {
|
|
1487
|
-
reconciliationManifest = JSON.parse(reconciliationRead.text);
|
|
1488
|
-
} catch (error) {
|
|
1489
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', `archived reconciliation manifest is invalid JSON: ${ error.message }`);
|
|
1490
|
-
}
|
|
1491
|
-
if (!validateReconciliationSchema(reconciliationManifest) || reconciliationManifest.change_id !== change.id) {
|
|
1492
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', 'archived reconciliation manifest no longer matches its accepted schema and Change', { errors: validateReconciliationSchema.errors || [] });
|
|
1493
|
-
}
|
|
1494
|
-
const reconciliationDigest = reconciliationRead.digest;
|
|
1495
|
-
let summaryReader;
|
|
1496
|
-
try {
|
|
1497
|
-
summaryReader = openStableProjectRead(rootDir, `${ relative }/archive-summary.md`);
|
|
1498
|
-
} catch (error) {
|
|
1499
|
-
throw new ChangeWorkflowError('ARCHIVE_PATH_UNSAFE', 'archive summary cannot be read safely', { cause: error.code || error.message });
|
|
1500
|
-
}
|
|
1501
|
-
const baselineHealthBefore = baselines.inspectBaseline(db, { rootDir });
|
|
1502
|
-
const breakGlass = Boolean(baselineHealthBefore.status !== 'pass' && change.kind === 'incident' && change.baseline_bypass?.mode === 'incident_break_glass');
|
|
1503
|
-
try {
|
|
1504
|
-
ops.tx(db, () => {
|
|
1505
|
-
db.prepare('UPDATE changes SET status = \'archived\', artifact_root = ?, updated_at = ?, closed_at = ? WHERE id = ?').run(relative, nowIso(), nowIso(), change.id);
|
|
1506
|
-
const artifactRows = db.prepare(`SELECT id, path, digest, content_hash, before_digest, metadata_json, provenance_json
|
|
1507
|
-
FROM artifacts WHERE change_id = ?`).all(change.id);
|
|
1508
|
-
for (const artifact of artifactRows) {
|
|
1509
|
-
const archivedPath = rebindRegistryPath(artifact.path, intent.source, relative);
|
|
1510
|
-
if (!archivedPath)
|
|
1511
|
-
continue;
|
|
1512
|
-
const rebound = rebind.byPath.get(archivedPath);
|
|
1513
|
-
if (rebound) {
|
|
1514
|
-
db.prepare(`UPDATE artifacts
|
|
1515
|
-
SET digest = ?, content_hash = ?, before_digest = COALESCE(before_digest, ?),
|
|
1516
|
-
after_digest = ?, metadata_json = ?, provenance_json = ?
|
|
1517
|
-
WHERE id = ?`).run(rebound.after_digest, rebound.after_digest, artifact.digest || artifact.content_hash || rebound.before_digest, rebound.after_digest, JSON.stringify(archiveRebindAuthorityValue(parseJson(artifact.metadata_json || '{}', 'artifacts.metadata_json'), intent.source, relative, rebind.digestMap)), JSON.stringify(archiveRebindAuthorityValue(parseJson(artifact.provenance_json || '{}', 'artifacts.provenance_json'), intent.source, relative, rebind.digestMap)), artifact.id);
|
|
1518
|
-
}
|
|
1519
|
-
artifacts.moveArtifactInTx(db, artifact.id, archivedPath, { rootDir });
|
|
1520
|
-
}
|
|
1521
|
-
const workflowOutputs = db.prepare(`SELECT ws.run_id, ws.step_id, ws.outputs_json, ws.evidence_json,
|
|
1522
|
-
ws.decisions_json, ws.semantic_records_json, ws.blockers_json
|
|
1523
|
-
FROM workflow_steps ws JOIN workflow_runs wr ON wr.id = ws.run_id
|
|
1524
|
-
WHERE wr.change_id = ?`).all(change.id);
|
|
1525
|
-
for (const step of workflowOutputs) {
|
|
1526
|
-
const rebound = Object.fromEntries([
|
|
1527
|
-
[
|
|
1528
|
-
'outputs_json',
|
|
1529
|
-
parseJson(step.outputs_json, 'workflow_steps.outputs_json')
|
|
1530
|
-
],
|
|
1531
|
-
[
|
|
1532
|
-
'evidence_json',
|
|
1533
|
-
parseJson(step.evidence_json, 'workflow_steps.evidence_json')
|
|
1534
|
-
],
|
|
1535
|
-
[
|
|
1536
|
-
'decisions_json',
|
|
1537
|
-
parseJson(step.decisions_json, 'workflow_steps.decisions_json')
|
|
1538
|
-
],
|
|
1539
|
-
[
|
|
1540
|
-
'semantic_records_json',
|
|
1541
|
-
parseJson(step.semantic_records_json, 'workflow_steps.semantic_records_json')
|
|
1542
|
-
],
|
|
1543
|
-
[
|
|
1544
|
-
'blockers_json',
|
|
1545
|
-
parseJson(step.blockers_json, 'workflow_steps.blockers_json')
|
|
1546
|
-
]
|
|
1547
|
-
].map(([field, value]) => [
|
|
1548
|
-
field,
|
|
1549
|
-
JSON.stringify(archiveRebindAuthorityValue(value, intent.source, relative, rebind.digestMap))
|
|
1550
|
-
]));
|
|
1551
|
-
db.prepare(`UPDATE workflow_steps
|
|
1552
|
-
SET outputs_json = ?, evidence_json = ?, decisions_json = ?,
|
|
1553
|
-
semantic_records_json = ?, blockers_json = ?, updated_at = ?
|
|
1554
|
-
WHERE run_id = ? AND step_id = ?`).run(rebound.outputs_json, rebound.evidence_json, rebound.decisions_json, rebound.semantic_records_json, rebound.blockers_json, nowIso(), step.run_id, step.step_id);
|
|
1555
|
-
}
|
|
1556
|
-
const contextRows = db.prepare(`SELECT id, manifest_path, manifest_hash, context_json
|
|
1557
|
-
FROM context_snapshots WHERE change_id = ?`).all(change.id);
|
|
1558
|
-
for (const snapshot of contextRows) {
|
|
1559
|
-
const archivedPath = rebindRegistryPath(snapshot.manifest_path, intent.source, relative);
|
|
1560
|
-
if (!archivedPath)
|
|
1561
|
-
continue;
|
|
1562
|
-
const rebound = rebind.byPath.get(archivedPath);
|
|
1563
|
-
db.prepare(`UPDATE context_snapshots
|
|
1564
|
-
SET manifest_path = ?, manifest_hash = ?, context_json = ? WHERE id = ?`).run(archivedPath, rebound?.after_digest || snapshot.manifest_hash, JSON.stringify(archiveRebindAuthorityValue(parseJson(snapshot.context_json || '{}', 'context_snapshots.context_json'), intent.source, relative, rebind.digestMap)), snapshot.id);
|
|
1565
|
-
}
|
|
1566
|
-
for (const decision of db.prepare(`SELECT id FROM decision_records
|
|
1567
|
-
WHERE scope_type = 'change' AND scope_id = ?`).all(change.id)) {
|
|
1568
|
-
const archivedPath = `${ relative }/decisions/${ decision.id }.json`;
|
|
1569
|
-
const rebound = rebind.byPath.get(archivedPath);
|
|
1570
|
-
if (!rebound) {
|
|
1571
|
-
throw new ChangeWorkflowError('ARCHIVE_DECISION_MISSING', `archive is missing Decision Record ${ decision.id }`);
|
|
1572
|
-
}
|
|
1573
|
-
const document = JSON.parse(Buffer.from(rebound.after_base64, 'base64').toString('utf8'));
|
|
1574
|
-
db.prepare(`UPDATE decision_records
|
|
1575
|
-
SET question = ?, recommendation = ?, selection = ?,
|
|
1576
|
-
effects_json = ?, non_goals_json = ?, owner = ?, source = ?,
|
|
1577
|
-
provenance_json = ?, applied_refs_json = ?, digest = ?,
|
|
1578
|
-
artifact_path = ?, updated_at = ?
|
|
1579
|
-
WHERE id = ?`).run(document.question, document.recommendation, document.selection, JSON.stringify(document.effects || {}), JSON.stringify(document.non_goals || []), document.owner, document.source, JSON.stringify(document.provenance || {}), JSON.stringify(document.applied_refs || []), document.digest, archivedPath, nowIso(), decision.id);
|
|
1580
|
-
}
|
|
1581
|
-
for (const envelope of db.prepare(`SELECT id FROM context_envelopes
|
|
1582
|
-
WHERE (scope_type = 'change' AND scope_id = ?)
|
|
1583
|
-
OR (scope_type = 'task' AND scope_id IN (
|
|
1584
|
-
SELECT id FROM tasks WHERE change_id = ?
|
|
1585
|
-
))`).all(change.id, change.id)) {
|
|
1586
|
-
const archivedPath = `${ relative }/authority/context-envelopes/${ envelope.id }.json`;
|
|
1587
|
-
const rebound = rebind.byPath.get(archivedPath);
|
|
1588
|
-
if (!rebound) {
|
|
1589
|
-
throw new ChangeWorkflowError('ARCHIVE_CONTEXT_MISSING', `archive is missing Context Envelope ${ envelope.id }`);
|
|
1590
|
-
}
|
|
1591
|
-
const document = JSON.parse(Buffer.from(rebound.after_base64, 'base64').toString('utf8'));
|
|
1592
|
-
db.prepare(`UPDATE context_envelopes
|
|
1593
|
-
SET digest = ?, file_digest = ?, payload_json = ?, artifact_path = ?
|
|
1594
|
-
WHERE id = ?`).run(document.digest, rebound.after_digest, JSON.stringify(document.envelope), archivedPath, envelope.id);
|
|
1595
|
-
}
|
|
1596
|
-
for (const packet of db.prepare(`SELECT id FROM worker_packets
|
|
1597
|
-
WHERE scope_type = 'task' AND status = 'assigned'
|
|
1598
|
-
AND scope_id IN (SELECT id FROM tasks WHERE change_id = ?)`).all(change.id)) {
|
|
1599
|
-
const archivedPath = `${ relative }/authority/worker-packets/${ packet.id }.json`;
|
|
1600
|
-
const rebound = rebind.byPath.get(archivedPath);
|
|
1601
|
-
if (!rebound) {
|
|
1602
|
-
throw new ChangeWorkflowError('ARCHIVE_WORKER_PACKET_MISSING', `archive is missing Worker Packet ${ packet.id }`);
|
|
1603
|
-
}
|
|
1604
|
-
const document = JSON.parse(Buffer.from(rebound.after_base64, 'base64').toString('utf8'));
|
|
1605
|
-
const decisionDigest = canonical.digest((document.accepted_decisions || []).map(decision => ({
|
|
1606
|
-
id: decision.id,
|
|
1607
|
-
digest: decision.digest,
|
|
1608
|
-
status: 'accepted'
|
|
1609
|
-
})));
|
|
1610
|
-
db.prepare(`UPDATE worker_packets
|
|
1611
|
-
SET context_digest = ?, task_digest = ?, decision_digest = ?,
|
|
1612
|
-
packet_digest = ?, file_digest = ?, packet_path = ?, output_path = ?
|
|
1613
|
-
WHERE id = ?`).run(document.context_envelope?.digest || null, document.task?.digest || null, decisionDigest, document.packet_digest, rebound.after_digest, archivedPath, document.output?.path, packet.id);
|
|
1614
|
-
}
|
|
1615
|
-
const checkpointRows = db.prepare(`SELECT *
|
|
1616
|
-
FROM stage_checkpoints
|
|
1617
|
-
WHERE (scope_type = 'change' AND scope_id = ?)
|
|
1618
|
-
OR (scope_type = 'task' AND scope_id IN (
|
|
1619
|
-
SELECT id FROM tasks WHERE change_id = ?
|
|
1620
|
-
))
|
|
1621
|
-
ORDER BY revision`).all(change.id, change.id);
|
|
1622
|
-
for (const row of checkpointRows) {
|
|
1623
|
-
const value = {
|
|
1624
|
-
stage: row.stage,
|
|
1625
|
-
scope_type: row.scope_type,
|
|
1626
|
-
scope_id: row.scope_id,
|
|
1627
|
-
revision: row.revision,
|
|
1628
|
-
payload: archiveRebindAuthorityValue(parseJson(row.payload_json || '{}', 'stage_checkpoints.payload_json'), intent.source, relative, rebind.digestMap),
|
|
1629
|
-
evidence: archiveRebindAuthorityValue(parseJson(row.evidence_json || '[]', 'stage_checkpoints.evidence_json'), intent.source, relative, rebind.digestMap),
|
|
1630
|
-
diagnostics: archiveRebindAuthorityValue(parseJson(row.diagnostics_json || '[]', 'stage_checkpoints.diagnostics_json'), intent.source, relative, rebind.digestMap),
|
|
1631
|
-
context_envelope_id: row.context_envelope_id,
|
|
1632
|
-
supersedes_id: row.supersedes_id
|
|
1633
|
-
};
|
|
1634
|
-
db.prepare(`UPDATE stage_checkpoints
|
|
1635
|
-
SET payload_json = ?, evidence_json = ?, diagnostics_json = ?, digest = ?,
|
|
1636
|
-
updated_at = ?
|
|
1637
|
-
WHERE id = ?`).run(JSON.stringify(value.payload), JSON.stringify(value.evidence), JSON.stringify(value.diagnostics), stageCheckpoints.checkpointDigest(value), nowIso(), row.id);
|
|
1638
|
-
}
|
|
1639
|
-
for (const task of db.prepare(`SELECT id, context_refs_json, docs_impact_json, ownership_json
|
|
1640
|
-
FROM tasks WHERE change_id = ?`).all(change.id)) {
|
|
1641
|
-
db.prepare(`UPDATE tasks
|
|
1642
|
-
SET context_refs_json = ?, docs_impact_json = ?, ownership_json = ?,
|
|
1643
|
-
updated_at = ?
|
|
1644
|
-
WHERE id = ?`).run(JSON.stringify(archiveRebindAuthorityValue(parseJson(task.context_refs_json || '[]', 'tasks.context_refs_json'), intent.source, relative, rebind.digestMap)), JSON.stringify(archiveRebindAuthorityValue(parseJson(task.docs_impact_json || '{}', 'tasks.docs_impact_json'), intent.source, relative, rebind.digestMap)), JSON.stringify(archiveRebindAuthorityValue(parseJson(task.ownership_json || '{}', 'tasks.ownership_json'), intent.source, relative, rebind.digestMap)), nowIso(), task.id);
|
|
1645
|
-
}
|
|
1646
|
-
for (const event of db.prepare(`SELECT id, payload_json FROM events
|
|
1647
|
-
WHERE change_id = ?
|
|
1648
|
-
OR task_id IN (SELECT id FROM tasks WHERE change_id = ?)`).all(change.id, change.id)) {
|
|
1649
|
-
db.prepare('UPDATE events SET payload_json = ? WHERE id = ?').run(JSON.stringify(archiveRebindAuthorityValue(parseJson(event.payload_json || '{}', 'events.payload_json'), intent.source, relative, rebind.digestMap)), event.id);
|
|
1650
|
-
}
|
|
1651
|
-
for (const run of db.prepare(`SELECT id, metadata_json, blockers_json, summary_json, approval_json
|
|
1652
|
-
FROM workflow_runs WHERE change_id = ?`).all(change.id)) {
|
|
1653
|
-
db.prepare(`UPDATE workflow_runs
|
|
1654
|
-
SET metadata_json = ?, blockers_json = ?, summary_json = ?, approval_json = ?
|
|
1655
|
-
WHERE id = ?`).run(JSON.stringify(archiveRebindAuthorityValue(parseJson(run.metadata_json || '{}', 'workflow_runs.metadata_json'), intent.source, relative, rebind.digestMap)), JSON.stringify(archiveRebindAuthorityValue(parseJson(run.blockers_json || '[]', 'workflow_runs.blockers_json'), intent.source, relative, rebind.digestMap)), JSON.stringify(archiveRebindAuthorityValue(parseJson(run.summary_json || '{}', 'workflow_runs.summary_json'), intent.source, relative, rebind.digestMap)), run.approval_json === null ? null : JSON.stringify(archiveRebindAuthorityValue(parseJson(run.approval_json, 'workflow_runs.approval_json'), intent.source, relative, rebind.digestMap)), run.id);
|
|
1656
|
-
}
|
|
1657
|
-
db.prepare(`UPDATE trace_links
|
|
1658
|
-
SET source_ref = REPLACE(source_ref, ?, ?),
|
|
1659
|
-
target_ref = REPLACE(target_ref, ?, ?)
|
|
1660
|
-
WHERE change_id = ?`).run(intent.source, relative, intent.source, relative, change.id);
|
|
1661
|
-
upsertArtifact(db, {
|
|
1662
|
-
change_id: change.id,
|
|
1663
|
-
kind: 'archive_summary',
|
|
1664
|
-
artifactPath: `${ relative }/archive-summary.md`,
|
|
1665
|
-
contentHash: summaryReader.digest,
|
|
1666
|
-
metadata: {
|
|
1667
|
-
baseline_updates: intent.baseline_updates,
|
|
1668
|
-
no_baseline_change_reason: intent.no_baseline_change_reason
|
|
1669
|
-
},
|
|
1670
|
-
terminal: true,
|
|
1671
|
-
rootDir
|
|
1672
|
-
});
|
|
1673
|
-
upsertArtifact(db, {
|
|
1674
|
-
change_id: change.id,
|
|
1675
|
-
kind: 'baseline_reconciliation',
|
|
1676
|
-
artifactPath: archivedReconciliationPath,
|
|
1677
|
-
contentHash: reconciliationDigest,
|
|
1678
|
-
metadata: {
|
|
1679
|
-
schema: reconciliationManifest.$schema,
|
|
1680
|
-
semantic_changes: reconciliationManifest.semantic_changes.map(item => item.id),
|
|
1681
|
-
resolved_gap_ids: reconciliationManifest.resolved_gap_ids,
|
|
1682
|
-
resolved_unknowns: reconciliationManifest.resolved_unknowns
|
|
1683
|
-
},
|
|
1684
|
-
terminal: true,
|
|
1685
|
-
rootDir
|
|
1686
|
-
});
|
|
1687
|
-
for (const artifact of db.prepare('SELECT id FROM artifacts WHERE change_id = ? ORDER BY id').all(change.id)) {
|
|
1688
|
-
artifacts.setArtifactStatusInTx(db, artifact.id, 'archived');
|
|
1689
|
-
}
|
|
1690
|
-
if (breakGlass) {
|
|
1691
|
-
const currentBaseline = baselines.readBaseline(db);
|
|
1692
|
-
if (currentBaseline) {
|
|
1693
|
-
baselines.appendGapInTx(db, {
|
|
1694
|
-
baseline_id: currentBaseline.id,
|
|
1695
|
-
gap: {
|
|
1696
|
-
id: `incident-${ change.id }-reconciliation`,
|
|
1697
|
-
category: 'baseline_blocker',
|
|
1698
|
-
status: 'open',
|
|
1699
|
-
blocking: true,
|
|
1700
|
-
summary: `Incident ${ change.id } was archived under break-glass and requires baseline reconciliation.`,
|
|
1701
|
-
evidence_refs: [relative],
|
|
1702
|
-
owner: change.baseline_bypass.approved_by
|
|
1703
|
-
}
|
|
1704
|
-
});
|
|
1705
|
-
}
|
|
1706
|
-
} else {
|
|
1707
|
-
baselines.reconcileBaseline(db, {
|
|
1708
|
-
baseline_updates: intent.baseline_updates,
|
|
1709
|
-
change_id: change.id,
|
|
1710
|
-
reconciliation: reconciliationManifest,
|
|
1711
|
-
accept_delivery_worktree: true
|
|
1712
|
-
}, { rootDir });
|
|
1713
|
-
const baselineHealth = baselines.inspectBaseline(db, { rootDir });
|
|
1714
|
-
if (baselineHealth.blockers.length > 0) {
|
|
1715
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_INCOMPLETE', `baseline reconciliation remains incomplete: ${ baselineHealth.blockers.join(', ') }`, { blockers: baselineHealth.blockers });
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
ops.appendEventInTx(db, {
|
|
1719
|
-
type: 'change_archived',
|
|
1720
|
-
change_id: change.id,
|
|
1721
|
-
payload: {
|
|
1722
|
-
archive_path: relative,
|
|
1723
|
-
baseline_updates: intent.baseline_updates,
|
|
1724
|
-
no_baseline_change_reason: intent.no_baseline_change_reason,
|
|
1725
|
-
reconciliation_path: archivedReconciliationPath,
|
|
1726
|
-
reconciliation_digest: reconciliationDigest,
|
|
1727
|
-
baseline_bypass: breakGlass
|
|
1728
|
-
}
|
|
1729
|
-
});
|
|
1730
|
-
summaryReader.verify();
|
|
1731
|
-
try {
|
|
1732
|
-
archiveJournal.verifyArchiveIntent(rootDir, journalIntent, { location: 'destination' });
|
|
1733
|
-
} catch (error) {
|
|
1734
|
-
throw new ChangeWorkflowError('ARCHIVE_PATH_UNSAFE', `archive transition changed physical identity before commit: ${ error.message }`, { cause: error.code || error.message });
|
|
1735
|
-
}
|
|
1736
|
-
});
|
|
1737
|
-
} finally {
|
|
1738
|
-
summaryReader.close();
|
|
1739
|
-
}
|
|
1740
|
-
return {
|
|
1741
|
-
change: readChange(db, change.id),
|
|
1742
|
-
archive_path: destination,
|
|
1743
|
-
baseline_bypass: breakGlass
|
|
1744
|
-
};
|
|
1745
|
-
}
|
|
1746
|
-
function archiveChange(db, input, {rootDir = process.cwd(), deferDeliveryCleanup = false} = {}) {
|
|
1747
|
-
const change = readChange(db, input.id);
|
|
1748
|
-
if (!change)
|
|
1749
|
-
throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${ input.id } not found`);
|
|
1750
|
-
if (!input.summary || String(input.summary).trim().length < 3) {
|
|
1751
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', 'archive summary required');
|
|
1752
|
-
}
|
|
1753
|
-
const allowedArchiveStatuses = [
|
|
1754
|
-
'active',
|
|
1755
|
-
'ready',
|
|
1756
|
-
'archived'
|
|
1757
|
-
];
|
|
1758
|
-
if (!allowedArchiveStatuses.includes(change.status)) {
|
|
1759
|
-
throw new ChangeWorkflowError('CHANGE_NOT_READY', `change ${ input.id } must converge before archive`);
|
|
1760
|
-
}
|
|
1761
|
-
if (change.status === 'archived') {
|
|
1762
|
-
if (!deferDeliveryCleanup) {
|
|
1763
|
-
deliveryTransaction.completeDeliveryTransaction({
|
|
1764
|
-
rootDir,
|
|
1765
|
-
changeId: change.id
|
|
1766
|
-
});
|
|
1767
|
-
}
|
|
1768
|
-
return {
|
|
1769
|
-
change,
|
|
1770
|
-
archive_path: safeRelativePath(rootDir, change.artifact_root),
|
|
1771
|
-
idempotent: true
|
|
1772
|
-
};
|
|
1773
|
-
}
|
|
1774
|
-
let packet = null;
|
|
1775
|
-
if (changePacket.readDeltaAuthority(db, change.id)) {
|
|
1776
|
-
packet = changePacket.deliveryEntries(db, change, { rootDir });
|
|
1777
|
-
}
|
|
1778
|
-
const requestedUpdates = Array.isArray(input.baseline_updates) ? input.baseline_updates.map(item => artifacts.normalizeRelativePath(item)) : null;
|
|
1779
|
-
const updates = packet ? packet.baseline_updates : requestedUpdates || [];
|
|
1780
|
-
if (packet && requestedUpdates && JSON.stringify([...requestedUpdates].sort()) !== JSON.stringify([...updates].sort())) {
|
|
1781
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_CONFLICT', 'baseline_updates must match the typed Change delta exactly');
|
|
1782
|
-
}
|
|
1783
|
-
const requestedNoChange = input.no_baseline_change_reason && String(input.no_baseline_change_reason).trim();
|
|
1784
|
-
const noChangeReason = packet ? packet.delta.value.no_semantic_change_reason || requestedNoChange || null : requestedNoChange;
|
|
1785
|
-
if (updates.length === 0 && !noChangeReason) {
|
|
1786
|
-
throw new ChangeWorkflowError('BASELINE_RECONCILIATION_REQUIRED', 'baseline updates or no-change reason required');
|
|
1787
|
-
}
|
|
1788
|
-
const reconciliation = packet ? generatePacketReconciliation(db, change, packet, rootDir) : updates.length === 0 && !String(input.reconciliation_path || '').trim() ? generateCallerNoChangeReconciliation(db, change, {
|
|
1789
|
-
summary: input.summary,
|
|
1790
|
-
no_baseline_change_reason: noChangeReason
|
|
1791
|
-
}, rootDir) : readReconciliationManifest(db, change, input, rootDir);
|
|
1792
|
-
materializeKernelArchiveAuthority(db, change, rootDir);
|
|
1793
|
-
let delivery = null;
|
|
1794
|
-
let prepared = null;
|
|
1795
|
-
try {
|
|
1796
|
-
if (packet) {
|
|
1797
|
-
delivery = deliveryTransaction.beginDeliveryTransaction({
|
|
1798
|
-
rootDir,
|
|
1799
|
-
changeId: change.id,
|
|
1800
|
-
entries: packet.entries
|
|
1801
|
-
});
|
|
1802
|
-
}
|
|
1803
|
-
for (const file of updates) {
|
|
1804
|
-
if (!fs.existsSync(safeRelativePath(rootDir, file))) {
|
|
1805
|
-
throw new ChangeWorkflowError('BASELINE_FILE_MISSING', `baseline update missing: ${ file }`);
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
prepared = archiveJournal.prepareArchiveMove({
|
|
1809
|
-
rootDir,
|
|
1810
|
-
change,
|
|
1811
|
-
summary: String(input.summary),
|
|
1812
|
-
baselineUpdates: updates,
|
|
1813
|
-
noBaselineChangeReason: noChangeReason || null,
|
|
1814
|
-
reconciliationPath: reconciliation.relative,
|
|
1815
|
-
reconciliationDigest: reconciliation.journal_intent?.reconciliation_digest || reconciliation.digest,
|
|
1816
|
-
reconciliationManifest: reconciliation.journal_intent?.reconciliation_manifest || reconciliation.manifest
|
|
1817
|
-
});
|
|
1818
|
-
} catch (error) {
|
|
1819
|
-
if (delivery) {
|
|
1820
|
-
try {
|
|
1821
|
-
deliveryTransaction.rollbackDeliveryTransaction({
|
|
1822
|
-
rootDir,
|
|
1823
|
-
changeId: change.id
|
|
1824
|
-
});
|
|
1825
|
-
} catch (rollbackError) {
|
|
1826
|
-
throw new ChangeWorkflowError('DELIVERY_RECOVERY_REQUIRED', error.message, {
|
|
1827
|
-
cause: error.code || error.message,
|
|
1828
|
-
rollback: rollbackError.code || rollbackError.message
|
|
1829
|
-
});
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
throw error;
|
|
1833
|
-
}
|
|
1834
|
-
const authoritativeIntent = {
|
|
1835
|
-
...prepared.intent,
|
|
1836
|
-
reconciliation_digest: reconciliation.digest,
|
|
1837
|
-
reconciliation_manifest: reconciliation.manifest
|
|
1838
|
-
};
|
|
1839
|
-
let result;
|
|
1840
|
-
try {
|
|
1841
|
-
result = finalizeArchive(db, authoritativeIntent, {
|
|
1842
|
-
rootDir,
|
|
1843
|
-
journalIntent: prepared.intent
|
|
1844
|
-
});
|
|
1845
|
-
} catch (error) {
|
|
1846
|
-
try {
|
|
1847
|
-
archiveJournal.rollbackArchiveIntent(rootDir, prepared.intent);
|
|
1848
|
-
} catch (rollbackError) {
|
|
1849
|
-
throw new ChangeWorkflowError('ARCHIVE_RECOVERY_REQUIRED', error.message, {
|
|
1850
|
-
cause: error.code || error.message,
|
|
1851
|
-
rollback: rollbackError.code || rollbackError.message
|
|
1852
|
-
});
|
|
1853
|
-
}
|
|
1854
|
-
if (delivery) {
|
|
1855
|
-
try {
|
|
1856
|
-
deliveryTransaction.rollbackDeliveryTransaction({
|
|
1857
|
-
rootDir,
|
|
1858
|
-
changeId: change.id
|
|
1859
|
-
});
|
|
1860
|
-
} catch (rollbackError) {
|
|
1861
|
-
throw new ChangeWorkflowError('DELIVERY_RECOVERY_REQUIRED', error.message, {
|
|
1862
|
-
cause: error.code || error.message,
|
|
1863
|
-
rollback: rollbackError.code || rollbackError.message
|
|
1864
|
-
});
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
throw error;
|
|
1868
|
-
}
|
|
1869
|
-
try {
|
|
1870
|
-
archiveJournal.completeArchiveIntent(rootDir, prepared.intent);
|
|
1871
|
-
} catch (error) {
|
|
1872
|
-
result.recovery_warning = `ARCHIVE_JOURNAL_CLEANUP_PENDING:${ error.message }`;
|
|
1873
|
-
}
|
|
1874
|
-
if (delivery && !deferDeliveryCleanup) {
|
|
1875
|
-
try {
|
|
1876
|
-
deliveryTransaction.completeDeliveryTransaction({
|
|
1877
|
-
rootDir,
|
|
1878
|
-
changeId: change.id
|
|
1879
|
-
});
|
|
1880
|
-
} catch (error) {
|
|
1881
|
-
result.recovery_warning = [
|
|
1882
|
-
result.recovery_warning,
|
|
1883
|
-
`DELIVERY_JOURNAL_CLEANUP_PENDING:${ error.message }`
|
|
1884
|
-
].filter(Boolean).join(';');
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
return result;
|
|
1888
|
-
}
|
|
1889
|
-
function recoverInterruptedArchives(db, {
|
|
1890
|
-
rootDir = process.cwd()
|
|
1891
|
-
} = {}) {
|
|
1892
|
-
const records = archiveJournal.listArchiveIntents(rootDir);
|
|
1893
|
-
const result = {
|
|
1894
|
-
found: records.length,
|
|
1895
|
-
resumed: 0,
|
|
1896
|
-
rolled_back: 0,
|
|
1897
|
-
cleaned: 0,
|
|
1898
|
-
failed: 0,
|
|
1899
|
-
items: []
|
|
1900
|
-
};
|
|
1901
|
-
for (const record of records) {
|
|
1902
|
-
if (record.error) {
|
|
1903
|
-
result.failed += 1;
|
|
1904
|
-
result.items.push({
|
|
1905
|
-
file: record.file,
|
|
1906
|
-
status: 'failed',
|
|
1907
|
-
error: record.error.code || record.error.message
|
|
1908
|
-
});
|
|
1909
|
-
continue;
|
|
1910
|
-
}
|
|
1911
|
-
const change = readChange(db, record.intent.change_id);
|
|
1912
|
-
try {
|
|
1913
|
-
if (change?.status === 'archived' && change.artifact_root === record.intent.destination) {
|
|
1914
|
-
archiveJournal.completeArchiveIntent(rootDir, record.intent);
|
|
1915
|
-
result.cleaned += 1;
|
|
1916
|
-
result.items.push({
|
|
1917
|
-
change_id: change.id,
|
|
1918
|
-
status: 'cleaned'
|
|
1919
|
-
});
|
|
1920
|
-
} else if ([
|
|
1921
|
-
'active',
|
|
1922
|
-
'ready'
|
|
1923
|
-
].includes(change?.status) && record.location === 'destination') {
|
|
1924
|
-
const reconciliation = readReconciliationManifest(db, change, {
|
|
1925
|
-
reconciliation_path: record.intent.reconciliation_path,
|
|
1926
|
-
baseline_updates: record.intent.baseline_updates
|
|
1927
|
-
}, rootDir);
|
|
1928
|
-
const authoritativeIntent = {
|
|
1929
|
-
...record.intent,
|
|
1930
|
-
reconciliation_digest: reconciliation.digest,
|
|
1931
|
-
reconciliation_manifest: reconciliation.manifest
|
|
1932
|
-
};
|
|
1933
|
-
finalizeArchive(db, authoritativeIntent, {
|
|
1934
|
-
rootDir,
|
|
1935
|
-
journalIntent: record.intent
|
|
1936
|
-
});
|
|
1937
|
-
archiveJournal.completeArchiveIntent(rootDir, record.intent);
|
|
1938
|
-
result.resumed += 1;
|
|
1939
|
-
result.items.push({
|
|
1940
|
-
change_id: change.id,
|
|
1941
|
-
status: 'resumed'
|
|
1942
|
-
});
|
|
1943
|
-
} else {
|
|
1944
|
-
archiveJournal.rollbackArchiveIntent(rootDir, record.intent);
|
|
1945
|
-
result.rolled_back += 1;
|
|
1946
|
-
result.items.push({
|
|
1947
|
-
change_id: record.intent.change_id,
|
|
1948
|
-
status: 'rolled_back'
|
|
1949
|
-
});
|
|
1950
|
-
}
|
|
1951
|
-
} catch (error) {
|
|
1952
|
-
try {
|
|
1953
|
-
archiveJournal.rollbackArchiveIntent(rootDir, record.intent);
|
|
1954
|
-
result.rolled_back += 1;
|
|
1955
|
-
result.items.push({
|
|
1956
|
-
change_id: record.intent.change_id,
|
|
1957
|
-
status: 'rolled_back',
|
|
1958
|
-
error: error.code || error.message
|
|
1959
|
-
});
|
|
1960
|
-
} catch (rollbackError) {
|
|
1961
|
-
result.failed += 1;
|
|
1962
|
-
result.items.push({
|
|
1963
|
-
change_id: record.intent.change_id,
|
|
1964
|
-
status: 'failed',
|
|
1965
|
-
error: `${ error.code || error.message }; ${ rollbackError.code || rollbackError.message }`
|
|
1966
|
-
});
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
const archivedChangeIds = new Set(db.prepare('SELECT id FROM changes WHERE status = \'archived\'').all().map(row => row.id));
|
|
1971
|
-
result.delivery = deliveryTransaction.recoverDeliveryTransactions({
|
|
1972
|
-
rootDir,
|
|
1973
|
-
archivedChangeIds
|
|
1974
|
-
});
|
|
1975
|
-
result.failed += result.delivery.failed;
|
|
1976
|
-
return result;
|
|
1977
|
-
}
|
|
1978
|
-
function supersedeKernelChange(db, input = {}, options = {}) {
|
|
1979
|
-
const sourceId = typeof input.id === 'string' ? input.id.trim() : '';
|
|
1980
|
-
const successorId = typeof input.successor_id === 'string' ? input.successor_id.trim() : '';
|
|
1981
|
-
if (!CHANGE_ID.test(sourceId) || !CHANGE_ID.test(successorId) || sourceId === successorId) {
|
|
1982
|
-
throw new ChangeWorkflowError('VALIDATION_ERROR', 'supersede requires distinct valid id and successor_id values');
|
|
1983
|
-
}
|
|
1984
|
-
const source = readChange(db, sourceId);
|
|
1985
|
-
if (!source) {
|
|
1986
|
-
throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${ sourceId } not found`);
|
|
1987
|
-
}
|
|
1988
|
-
if (![
|
|
1989
|
-
'archived',
|
|
1990
|
-
'cancelled'
|
|
1991
|
-
].includes(source.status)) {
|
|
1992
|
-
throw new ChangeWorkflowError('CHANGE_SUCCESSOR_NOT_ALLOWED', `change ${ sourceId } is ${ source.status }; revise mutable Changes instead`);
|
|
1993
|
-
}
|
|
1994
|
-
const created = createChange(db, {
|
|
1995
|
-
id: successorId,
|
|
1996
|
-
title: input.title || `${ source.title } successor`,
|
|
1997
|
-
kind: input.kind || source.kind,
|
|
1998
|
-
intent: input.intent || source.intent,
|
|
1999
|
-
docs_impact: source.docs_impact,
|
|
2000
|
-
provider_refs: source.provider_refs,
|
|
2001
|
-
contract: source.contract,
|
|
2002
|
-
classification: source.classification,
|
|
2003
|
-
research_disposition: source.research_disposition,
|
|
2004
|
-
base_commit: source.base_commit,
|
|
2005
|
-
supersedes_id: source.id
|
|
2006
|
-
}, options);
|
|
2007
|
-
return {
|
|
2008
|
-
source,
|
|
2009
|
-
successor: created.change,
|
|
2010
|
-
intent_path: created.intent_path,
|
|
2011
|
-
diagnostics: created.diagnostics || []
|
|
2012
|
-
};
|
|
2013
|
-
}
|
|
2014
|
-
module.exports = {
|
|
2015
|
-
ChangeWorkflowError,
|
|
2016
|
-
createChange,
|
|
2017
|
-
supersedeChange: supersedeKernelChange,
|
|
2018
|
-
updateChange,
|
|
2019
|
-
archiveChange,
|
|
2020
|
-
readChange,
|
|
2021
|
-
listChanges,
|
|
2022
|
-
recordDelta,
|
|
2023
|
-
recordDocumentationReconciliation,
|
|
2024
|
-
normalizeDocsImpact,
|
|
2025
|
-
normalizeProviderRefs,
|
|
2026
|
-
normalizeBaselineBypass,
|
|
2027
|
-
assertTaskCreationAllowed,
|
|
2028
|
-
recoverInterruptedArchives
|
|
2029
|
-
};
|