agentera 3.0.0-dev.51 → 3.0.0-dev.69
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/README.md +5 -5
- package/bundle/CHANGELOG.md +21 -11
- package/bundle/UPGRADE.md +23 -7
- package/bundle/references/adapters/package-publication.json +68 -17
- package/bundle/references/adapters/package-registry.yaml +145 -145
- package/bundle/references/analysis/verification-policy.yaml +93 -18
- package/bundle/references/artifacts/state-storage-authority.yaml +50 -11
- package/bundle/skills/agentera/SKILL.md +7 -5
- package/bundle/skills/agentera/capabilities/build/schemas/artifacts.yaml +7 -6
- package/bundle/skills/agentera/capabilities/build/schemas/exit.yaml +6 -5
- package/bundle/skills/agentera/capabilities/build/schemas/validation.yaml +18 -10
- package/bundle/skills/agentera/schemas/artifacts/plan.yaml +3 -0
- package/bundle/skills/agentera/schemas/artifacts/progress.yaml +8 -6
- package/dist/capabilities/build/instructions.js +8 -5
- package/dist/cli/capabilityContext/build.js +12 -3
- package/dist/cli/capabilityContext/planState.js +6 -2
- package/dist/cli/capabilityContext/startupAggregation.js +1 -1
- package/dist/cli/commands/doctor.js +1 -1
- package/dist/cli/commands/prime/briefOrientation.js +0 -1
- package/dist/cli/commands/prime/collectOrientationState.js +13 -6
- package/dist/cli/commands/prime/orientationOutput.js +27 -5
- package/dist/cli/commands/state/write.js +4 -1
- package/dist/cli/dispatch/index.js +1 -1
- package/dist/cli/help.js +1 -0
- package/dist/cli/migrationRequired.js +40 -5
- package/dist/registries/activationTuples.js +3 -2
- package/dist/registries/packagePublication.js +51 -7
- package/dist/state/entityMigrationPreview.js +3 -9
- package/dist/state/entityPublicationContext.js +56 -3
- package/dist/state/planEntities.js +49 -4
- package/dist/state/progressWritePolicy.js +33 -0
- package/dist/state/stateMode.js +138 -1
- package/dist/state/todoDocsEntities.js +62 -7
- package/dist/state/todoReconciliationActivation.js +46 -4
- package/dist/state/todoReconciliationInspection.js +5 -3
- package/dist/state/todoReconciliationRepair.js +149 -1
- package/dist/state/todoReconciliationTransaction.js +20 -7
- package/dist/state/write/explain.js +17 -3
- package/dist/state/write/input.js +25 -1
- package/dist/state/write/runtimeOperations.js +4 -2
- package/dist/state/write/transaction.js +6 -1
- package/dist/validate/activationArtifactEvidence.js +49 -41
- package/dist/validate/activationArtifactEvidenceTypes.js +12 -0
- package/dist/validate/activationEvidenceManifest.js +26 -10
- package/package.json +11 -6
package/dist/state/stateMode.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { resolveSourceRoot } from "../core/sourceRoot.js";
|
|
4
|
-
import { loadYamlMapping } from "../core/yaml.js";
|
|
5
|
+
import { dumpYamlMapping, loadYamlMapping } from "../core/yaml.js";
|
|
5
6
|
import { EntityPublicationContext } from "./entityPublicationContext.js";
|
|
6
7
|
import { validateRealProjectRoot } from "./projectRoot.js";
|
|
7
8
|
import { readProjectFileSnapshot } from "./safeProjectFile.js";
|
|
@@ -44,6 +45,142 @@ function readStableMarker(root, markerPath) {
|
|
|
44
45
|
}
|
|
45
46
|
return snapshot.bytes;
|
|
46
47
|
}
|
|
48
|
+
function pathKind(root, relativePath) {
|
|
49
|
+
const absolute = path.join(root.path, ...relativePath.split("/"));
|
|
50
|
+
try {
|
|
51
|
+
const stat = fs.lstatSync(absolute);
|
|
52
|
+
if (stat.isSymbolicLink())
|
|
53
|
+
return "unsafe";
|
|
54
|
+
if (stat.isFile())
|
|
55
|
+
return "file";
|
|
56
|
+
if (stat.isDirectory())
|
|
57
|
+
return "directory";
|
|
58
|
+
return "unsafe";
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code === "ENOENT")
|
|
62
|
+
return "missing";
|
|
63
|
+
return "unsafe";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function isGitWorktreeRoot(root) {
|
|
67
|
+
const env = { ...process.env };
|
|
68
|
+
for (const name of ["GIT_INDEX_FILE", "GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR"])
|
|
69
|
+
delete env[name];
|
|
70
|
+
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
71
|
+
cwd: root.path,
|
|
72
|
+
env,
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
});
|
|
75
|
+
if (result.error || result.status !== 0)
|
|
76
|
+
return false;
|
|
77
|
+
try {
|
|
78
|
+
return fs.realpathSync(String(result.stdout).trim()) === root.path;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function classifyMarkerAbsentProject(root, markerPath) {
|
|
85
|
+
const stateRoot = pathKind(root, ".agentera");
|
|
86
|
+
if (stateRoot === "missing") {
|
|
87
|
+
return {
|
|
88
|
+
state: isGitWorktreeRoot(root) ? "fresh_uninitialized" : "unknown",
|
|
89
|
+
root,
|
|
90
|
+
markerPath,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (stateRoot !== "directory") {
|
|
94
|
+
return {
|
|
95
|
+
state: "corrupt",
|
|
96
|
+
root,
|
|
97
|
+
markerPath,
|
|
98
|
+
reason: "Agentera state root '.agentera' is not a safe directory",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const entities = pathKind(root, ".agentera/entities");
|
|
102
|
+
if (entities !== "missing" && entities !== "directory") {
|
|
103
|
+
return {
|
|
104
|
+
state: "corrupt",
|
|
105
|
+
root,
|
|
106
|
+
markerPath,
|
|
107
|
+
reason: "Agentera entity root '.agentera/entities' is not a safe directory",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (entities === "directory")
|
|
111
|
+
return { state: "partial", root, markerPath };
|
|
112
|
+
const aggregates = ["plan.yaml", "progress.yaml", "decisions.yaml", "health.yaml"]
|
|
113
|
+
.map((name) => pathKind(root, `.agentera/${name}`));
|
|
114
|
+
if (aggregates.includes("unsafe")) {
|
|
115
|
+
return {
|
|
116
|
+
state: "corrupt",
|
|
117
|
+
root,
|
|
118
|
+
markerPath,
|
|
119
|
+
reason: "a recognized Agentera aggregate is not a safe regular file",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const aggregateCount = aggregates.filter((kind) => kind === "file").length;
|
|
123
|
+
if (aggregateCount >= 2)
|
|
124
|
+
return { state: "legacy", root, markerPath };
|
|
125
|
+
if (aggregateCount === 1)
|
|
126
|
+
return { state: "partial", root, markerPath };
|
|
127
|
+
return { state: "unknown", root, markerPath };
|
|
128
|
+
}
|
|
129
|
+
/** Classify local state without creating, repairing, or adopting project files. */
|
|
130
|
+
export function classifyProjectState(projectRoot, sourceRoot = resolveSourceRoot()) {
|
|
131
|
+
const root = validateRealProjectRoot(projectRoot);
|
|
132
|
+
const declared = contract(sourceRoot);
|
|
133
|
+
let bytes;
|
|
134
|
+
try {
|
|
135
|
+
bytes = readStableMarker(root, declared.markerPath);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
return {
|
|
139
|
+
state: "corrupt",
|
|
140
|
+
root,
|
|
141
|
+
markerPath: declared.markerPath,
|
|
142
|
+
reason: error.message,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (bytes === null)
|
|
146
|
+
return classifyMarkerAbsentProject(root, declared.markerPath);
|
|
147
|
+
try {
|
|
148
|
+
const document = loadYamlMapping(bytes.toString("utf8"));
|
|
149
|
+
if (document.schemaVersion !== declared.schemaVersion || document.mode !== declared.mode) {
|
|
150
|
+
return {
|
|
151
|
+
state: "corrupt",
|
|
152
|
+
root,
|
|
153
|
+
markerPath: declared.markerPath,
|
|
154
|
+
reason: `state mode marker '${declared.markerPath}' must declare schemaVersion '${declared.schemaVersion}' and mode '${declared.mode}'; restore the durable migration marker before retrying`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
return {
|
|
160
|
+
state: "corrupt",
|
|
161
|
+
root,
|
|
162
|
+
markerPath: declared.markerPath,
|
|
163
|
+
reason: `state mode marker '${declared.markerPath}' is corrupt: ${error.message}; restore the durable migration marker before retrying`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return { state: "entities", root, markerPath: declared.markerPath, markerBytes: bytes };
|
|
167
|
+
}
|
|
168
|
+
export function freshEntityStateMarker(sourceRoot = resolveSourceRoot()) {
|
|
169
|
+
const declared = contract(sourceRoot);
|
|
170
|
+
return { schemaVersion: declared.schemaVersion, mode: declared.mode };
|
|
171
|
+
}
|
|
172
|
+
export function freshPlanInitialization(projectRoot, sourceRoot = resolveSourceRoot()) {
|
|
173
|
+
const classified = classifyProjectState(projectRoot, sourceRoot);
|
|
174
|
+
if (classified.state !== "fresh_uninitialized")
|
|
175
|
+
return null;
|
|
176
|
+
const marker = freshEntityStateMarker(sourceRoot);
|
|
177
|
+
return {
|
|
178
|
+
root: classified.root,
|
|
179
|
+
markerPath: classified.markerPath,
|
|
180
|
+
marker,
|
|
181
|
+
markerBytes: dumpYamlMapping(marker),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
47
184
|
function detectValidatedStateMode(projectRoot, sourceRoot) {
|
|
48
185
|
const root = validateRealProjectRoot(projectRoot);
|
|
49
186
|
const declared = contract(sourceRoot);
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { resolveSourceRoot } from "../core/sourceRoot.js";
|
|
5
5
|
import { canonicalRecordJson } from "./archiveDiscovery.js";
|
|
6
6
|
import { StateRetrievalFailure } from "./directRetrieval.js";
|
|
7
|
-
import { allocateEntityId, assertEntityDiscoveryOrigin, canonicalEntityEnvelopeBytes, canonicalEntityRecordViolations, discoverEntities, entityExactGetMaxBytes, exactDiscoveredEntityBytes, publishEntityUnderLock, replaceEntityUnderLock, validateEntityState, withEntityWriterLock
|
|
7
|
+
import { allocateEntityId, assertEntityDiscoveryOrigin, canonicalEntityEnvelopeBytes, canonicalEntityRecordViolations, discoverEntities, entityExactGetMaxBytes, exactDiscoveredEntityBytes, publishEntityUnderLock, replaceEntityUnderLock, validateEntityState, withEntityWriterLock } from "./entityStorage.js";
|
|
8
8
|
import { detectStateModeBinding } from "./stateMode.js";
|
|
9
9
|
import { reject, StateWriteInputError } from "./write/errors.js";
|
|
10
10
|
import { TODO_SEVERITIES, TODO_STATUSES, todoDocsRecordViolations, todoInputViolations } from "./todoDocsEntityValidation.js";
|
|
@@ -18,8 +18,8 @@ import { parseTodoMarkdownListItem, renderTodoPublicRecord } from "../cli/todoMa
|
|
|
18
18
|
import { evaluateTodoReadinessQueue } from "../cli/todoReadinessSelection.js";
|
|
19
19
|
import { artifactSchemasDir, loadArtifactRecord, registryModelPath, resolveArtifactPath } from "../registries/artifactRegistry.js";
|
|
20
20
|
import { inspectTodoReconciliation, publishTodoReconciliation, recoverTodoReconciliation, todoCreateRequestSha256 } from "./todoReconciliationTransaction.js";
|
|
21
|
-
import { loadTodoReconciliationActivation, todoLegacyRowFingerprint, todoReconciliationActivationBytes, TODO_RECONCILIATION_ACTIVATION_PATH, TODO_RECONCILIATION_ITEM_LIMIT, TODO_ACTIVATION_APPLY_COMMAND, TODO_ACTIVATION_PREVIEW_COMMAND, TODO_ACTIVATION_RISK_LIMIT, TODO_REPAIR_APPLY_COMMAND, TODO_REPAIR_PREVIEW_COMMAND, todoActivationEffect, todoRepairEffect, unchangedTodoActivationEffect
|
|
22
|
-
import { planTodoRepair } from "./todoReconciliationRepair.js";
|
|
21
|
+
import { loadTodoReconciliationActivation, todoLegacyRowFingerprint, todoReconciliationActivationBytes, TODO_RECONCILIATION_ACTIVATION_PATH, TODO_RECONCILIATION_ITEM_LIMIT, TODO_ACTIVATION_APPLY_COMMAND, TODO_ACTIVATION_PREVIEW_COMMAND, TODO_ACTIVATION_RISK_LIMIT, TODO_OWNER_CORRECTION_APPLY_COMMAND, TODO_OWNER_CORRECTION_PREVIEW_COMMAND, TODO_REPAIR_APPLY_COMMAND, TODO_REPAIR_PREVIEW_COMMAND, todoActivationEffect, todoOwnerCorrectionEffect, todoRepairEffect, unchangedTodoActivationEffect } from "./todoReconciliationActivation.js";
|
|
22
|
+
import { normalizeTodoOwnerCorrectionEvidence, planTodoOwnerCorrection, planTodoRepair } from "./todoReconciliationRepair.js";
|
|
23
23
|
import { readTodoMarkdown, renderManagedMarkdown } from "./todoMarkdownProjection.js";
|
|
24
24
|
import { inactiveTodoActivationSafety, rejectUnsafeInactiveTodoActivation, unsafeInactiveDuplicateDiagnosis } from "./todoActivationSafety.js";
|
|
25
25
|
const ID = /^[a-z]{10}$/;
|
|
@@ -412,6 +412,7 @@ function withBaseline(record, value) { const publicValue = Object.fromEntries(Ob
|
|
|
412
412
|
function inactiveTodoMutation() { reject({ class: "conflict", message: "TODO reconciliation is inactive; ordinary TODO mutations cannot activate it implicitly", syntax: TODO_ACTIVATION_PREVIEW_COMMAND, example: TODO_ACTIVATION_APPLY_COMMAND, recovery: `Run exactly '${TODO_ACTIVATION_PREVIEW_COMMAND}', review every reported effect, then run exactly '${TODO_ACTIVATION_APPLY_COMMAND}'; no state was changed.` }); }
|
|
413
413
|
function activationEnvelope(effect, dryRun, replay, transactionId, targets, recovered) { return { schemaVersion: "agentera.stateWrite.v1", command: "state todo activate", status: "pass", path: TODO_RECONCILIATION_ACTIVATION_PATH, artifact: "todo", operation: { verb: "activate", dry_run: dryRun, idempotent_replay: replay }, validation: { status: "pass", violations: [] }, activation: effect, apply_command: TODO_ACTIVATION_APPLY_COMMAND.replace("EFFECT_SHA256", String(effect.effect_sha256)), reconciliation: { transaction_id: transactionId, targets, recovered } }; }
|
|
414
414
|
function repairEnvelope(effect, dryRun, replay, transactionId, targets, recovered) { return { schemaVersion: "agentera.stateWrite.v1", command: "state todo repair", status: "pass", path: TODO_RECONCILIATION_ACTIVATION_PATH, artifact: "todo", operation: { verb: "repair", dry_run: dryRun, idempotent_replay: replay }, validation: { status: "pass", violations: [] }, repair: effect, apply_command: TODO_REPAIR_APPLY_COMMAND.replace("EFFECT_SHA256", String(effect.effect_sha256)), reconciliation: { transaction_id: transactionId, targets, recovered } }; }
|
|
415
|
+
function ownerCorrectionEnvelope(effect, dryRun, replay, transactionId, targets, recovered) { return { schemaVersion: "agentera.stateWrite.v1", command: "state todo correct-owners", status: "pass", path: TODO_RECONCILIATION_ACTIVATION_PATH, artifact: "todo", operation: { verb: "correct-owners", dry_run: dryRun, idempotent_replay: replay }, validation: { status: "pass", violations: [] }, correction: effect, apply_command: TODO_OWNER_CORRECTION_APPLY_COMMAND.replace("EFFECT_SHA256", String(effect.effect_sha256)), reconciliation: { transaction_id: transactionId, targets, recovered } }; }
|
|
415
416
|
function envelope(command, entity, artifact, record, dryRun) { return { schemaVersion: "agentera.stateWrite.v1", command, status: "pass", path: entity.path, id: entity.id, artifact, record, operation: { verb: command.split(" ").at(-1), dry_run: dryRun, idempotent_replay: entity.replay }, validation: { status: "pass", violations: [] } }; }
|
|
416
417
|
function targetPath(root, sourceRoot, artifact, id) {
|
|
417
418
|
const model = definition(artifact);
|
|
@@ -665,12 +666,14 @@ export function mutateTodoDocsEntity(req, options = {}) {
|
|
|
665
666
|
const sourceBinding = { kind: "project", projectRoot: context.validatedRoot };
|
|
666
667
|
context.assertValid();
|
|
667
668
|
const todoBinding = artifact === "todo" ? todoReconciliationBinding(req.projectRoot, sourceRoot) : null;
|
|
669
|
+
const correctingOwners = artifact === "todo" && req.spec.verb === "correct-owners";
|
|
670
|
+
const ownerEvidence = correctingOwners ? normalizeTodoOwnerCorrectionEvidence(req.input ?? {}) : null;
|
|
668
671
|
const pending = todoBinding ? inspectTodoReconciliation(pinnedRoot, todoBinding) : [];
|
|
669
672
|
const initialActivation = artifact === "todo" ? loadTodoReconciliationActivation(pinnedRoot) : null;
|
|
670
|
-
if (artifact === "todo" && ["activate", "repair"].includes(req.spec.verb)) {
|
|
673
|
+
if (artifact === "todo" && ["activate", "repair", "correct-owners"].includes(req.spec.verb)) {
|
|
671
674
|
const repairing = req.spec.verb === "repair";
|
|
672
|
-
const previewCommand = repairing ? TODO_REPAIR_PREVIEW_COMMAND : TODO_ACTIVATION_PREVIEW_COMMAND;
|
|
673
|
-
const applyCommand = repairing ? TODO_REPAIR_APPLY_COMMAND : TODO_ACTIVATION_APPLY_COMMAND;
|
|
675
|
+
const previewCommand = correctingOwners ? TODO_OWNER_CORRECTION_PREVIEW_COMMAND : repairing ? TODO_REPAIR_PREVIEW_COMMAND : TODO_ACTIVATION_PREVIEW_COMMAND;
|
|
676
|
+
const applyCommand = correctingOwners ? TODO_OWNER_CORRECTION_APPLY_COMMAND : repairing ? TODO_REPAIR_APPLY_COMMAND : TODO_ACTIVATION_APPLY_COMMAND;
|
|
674
677
|
if (repairing && !initialActivation)
|
|
675
678
|
reject({ class: "conflict", message: "TODO repair requires an existing reconciliation activation marker", recovery: `Use '${TODO_ACTIVATION_PREVIEW_COMMAND}' for an inactive project; no state was changed.` });
|
|
676
679
|
if (req.dryRun && (req.values.confirmed === true || req.values.effect_sha256 !== undefined))
|
|
@@ -687,7 +690,8 @@ export function mutateTodoDocsEntity(req, options = {}) {
|
|
|
687
690
|
const recoveryReceipts = todoBinding && !req.dryRun
|
|
688
691
|
? recoverTodoReconciliation(context, sourceRoot, todoBinding, {
|
|
689
692
|
createRequestSha256,
|
|
690
|
-
...(["activate", "repair"].includes(req.spec.verb) ? { activationEffectSha256: String(req.values.effect_sha256) } : {}),
|
|
693
|
+
...(["activate", "repair", "correct-owners"].includes(req.spec.verb) ? { activationEffectSha256: String(req.values.effect_sha256) } : {}),
|
|
694
|
+
...(correctingOwners ? { ownerMappingSha256: ownerEvidence.sha256 } : {}),
|
|
691
695
|
beforeCommit: () => assertState(pinnedRoot, sourceRoot, sourceBinding),
|
|
692
696
|
})
|
|
693
697
|
: [];
|
|
@@ -767,6 +771,57 @@ export function mutateTodoDocsEntity(req, options = {}) {
|
|
|
767
771
|
context.assertValid();
|
|
768
772
|
return activationEnvelope(effect, false, false, transaction.id, transaction.targetCount, recovered);
|
|
769
773
|
}
|
|
774
|
+
if (req.spec.verb === "correct-owners") {
|
|
775
|
+
if (activation) {
|
|
776
|
+
if (!req.dryRun && activation.effect_operation === "correct-owners" && activation.effect_sha256 === req.values.effect_sha256 && activation.owner_mapping_sha256 === ownerEvidence.sha256) {
|
|
777
|
+
const replayEffect = todoOwnerCorrectionEffect({ counts: { matched: 0, converted: 0, retained: 0, duplicate: 0, stale: 0, conflicting: 0 }, items: [], omitted_count: 0 }, ownerEvidence.sha256, [], publicRelative, markdownBefore, markdown);
|
|
778
|
+
replayEffect.effect_sha256 = activation.effect_sha256;
|
|
779
|
+
return ownerCorrectionEnvelope(replayEffect, false, true, null, 0, recovered);
|
|
780
|
+
}
|
|
781
|
+
reject({ class: "conflict", message: "TODO owner correction requires marker-absent unsafe-inactive state", recovery: "Use state todo repair only for an unsafe active reconciliation, or retry the exact owner-correction apply command when its stored effect and owner mapping match; no state was changed." });
|
|
782
|
+
}
|
|
783
|
+
let unsafe = true;
|
|
784
|
+
try {
|
|
785
|
+
unsafe = !inactiveTodoActivationSafety(managedRows(markdown, null, todoEntities), todoEntities).safe;
|
|
786
|
+
}
|
|
787
|
+
catch {
|
|
788
|
+
unsafe = true;
|
|
789
|
+
}
|
|
790
|
+
if (!unsafe)
|
|
791
|
+
reject({ class: "conflict", message: "TODO owner correction requires marker-absent unsafe-inactive state", recovery: `Use '${TODO_ACTIVATION_PREVIEW_COMMAND}' for a safe inactive project; no state was changed.` });
|
|
792
|
+
const plan = planTodoOwnerCorrection(markdown, todoEntities, ownerEvidence);
|
|
793
|
+
for (const [todoId, record] of plan.records) {
|
|
794
|
+
const violations = recordViolations("todo", record, sourceRoot);
|
|
795
|
+
if (violations.length)
|
|
796
|
+
reject({ class: "schema_violation", message: "todo entity input is invalid", violations });
|
|
797
|
+
if (record.readiness !== undefined)
|
|
798
|
+
assertTodoReferences(todoId, record, [...plan.records].map(([entityId, entityRecord]) => ({ boundary: TODO.boundary, id: entityId, record: entityRecord })));
|
|
799
|
+
}
|
|
800
|
+
let activationBytesAfter = todoReconciliationActivationBytes([], "0".repeat(64), "correct-owners", ownerEvidence.sha256);
|
|
801
|
+
const activationAfter = JSON.parse(activationBytesAfter);
|
|
802
|
+
const finalRows = managedRows(plan.rendered, activationAfter, todoEntities).rows;
|
|
803
|
+
for (const [todoId, record] of plan.records) {
|
|
804
|
+
const row = finalRows.get(todoId);
|
|
805
|
+
plan.records.set(todoId, withBaseline(record, row ? rowSnapshot(row, record) : { present: false }));
|
|
806
|
+
}
|
|
807
|
+
const targets = todoEntities.map((entity) => ({ path: entity.relativePath, before: exactDiscoveredEntityBytes(entity), after: canonicalEntityEnvelopeBytes({ id: entity.id, artifact: "todo", record: plan.records.get(entity.id), migrationProvenance: entity.migrationProvenance ?? undefined }) }));
|
|
808
|
+
targets.push({ path: TODO_RECONCILIATION_ACTIVATION_PATH, before: null, after: activationBytesAfter });
|
|
809
|
+
targets.push({ path: publicRelative, before: publicExists ? markdownBefore : null, after: plan.rendered });
|
|
810
|
+
const preliminaryEffect = todoOwnerCorrectionEffect(plan.diagnosis, ownerEvidence.sha256, targets, publicRelative, markdownBefore, plan.rendered);
|
|
811
|
+
activationBytesAfter = todoReconciliationActivationBytes([], String(preliminaryEffect.effect_sha256), "correct-owners", ownerEvidence.sha256);
|
|
812
|
+
targets.find((target) => target.path === TODO_RECONCILIATION_ACTIVATION_PATH).after = activationBytesAfter;
|
|
813
|
+
const effect = todoOwnerCorrectionEffect(plan.diagnosis, ownerEvidence.sha256, targets, publicRelative, markdownBefore, plan.rendered);
|
|
814
|
+
if (effect.effect_sha256 !== preliminaryEffect.effect_sha256)
|
|
815
|
+
throw new Error("TODO owner correction effect authorization is not self-consistent");
|
|
816
|
+
if (req.dryRun)
|
|
817
|
+
return ownerCorrectionEnvelope(effect, true, false, null, effect.targets.length, recovered);
|
|
818
|
+
if (req.values.effect_sha256 !== effect.effect_sha256)
|
|
819
|
+
reject({ class: "conflict", message: "TODO owner correction effects changed after preview", syntax: TODO_OWNER_CORRECTION_PREVIEW_COMMAND, example: TODO_OWNER_CORRECTION_APPLY_COMMAND, recovery: `Rerun exactly '${TODO_OWNER_CORRECTION_PREVIEW_COMMAND}', review the changed bounded effects, then run its new exact apply_command; no state was changed.` });
|
|
820
|
+
const transaction = publishTodoReconciliation(context, sourceRoot, todoBinding, targets, { activationEffectSha256: String(effect.effect_sha256), ownerMappingSha256: ownerEvidence.sha256, interruptAfterTarget: options.interruptAfterTarget, beforeCommit: () => { assertState(pinnedRoot, sourceRoot, sourceBinding); const currentBinding = todoReconciliationBinding(req.projectRoot, sourceRoot); if (currentBinding.publicPath !== todoBinding.publicPath || currentBinding.mappingSha256 !== todoBinding.mappingSha256)
|
|
821
|
+
reject({ class: "conflict", message: "TODO reconciliation mapping changed during owner correction publication", recovery: "Preserve the changed docs mapping and retry owner correction after every transaction target is restored; no mapping bytes were overwritten." }); } });
|
|
822
|
+
context.assertValid();
|
|
823
|
+
return ownerCorrectionEnvelope(effect, false, false, transaction.id, transaction.targetCount, recovered);
|
|
824
|
+
}
|
|
770
825
|
if (req.spec.verb === "repair") {
|
|
771
826
|
if (!activation || !loadedActivation)
|
|
772
827
|
throw new Error("TODO repair lost its required activation marker");
|
|
@@ -13,7 +13,10 @@ export const TODO_ACTIVATION_PREVIEW_COMMAND = `${CANONICAL_DEVELOPMENT_CLI} sta
|
|
|
13
13
|
export const TODO_ACTIVATION_APPLY_COMMAND = `${CANONICAL_DEVELOPMENT_CLI} state todo activate --effect-sha256 EFFECT_SHA256 --yes --format json`;
|
|
14
14
|
export const TODO_REPAIR_PREVIEW_COMMAND = `${CANONICAL_DEVELOPMENT_CLI} state todo repair --dry-run --format json`;
|
|
15
15
|
export const TODO_REPAIR_APPLY_COMMAND = `${CANONICAL_DEVELOPMENT_CLI} state todo repair --effect-sha256 EFFECT_SHA256 --yes --format json`;
|
|
16
|
-
export const
|
|
16
|
+
export const TODO_OWNER_CORRECTION_INPUT_VERSION = "agentera.todoOwnerCorrection.v1";
|
|
17
|
+
export const TODO_OWNER_CORRECTION_PREVIEW_COMMAND = `${CANONICAL_DEVELOPMENT_CLI} state todo correct-owners --input OWNER_MAPPING.yaml --dry-run --format json`;
|
|
18
|
+
export const TODO_OWNER_CORRECTION_APPLY_COMMAND = `${CANONICAL_DEVELOPMENT_CLI} state todo correct-owners --input OWNER_MAPPING.yaml --effect-sha256 EFFECT_SHA256 --yes --format json`;
|
|
19
|
+
export const TODO_UNSAFE_INACTIVE_RECOVERY = `Owner correction required: run exactly '${TODO_OWNER_CORRECTION_PREVIEW_COMMAND}', then use its exact apply_command; no state was changed.`;
|
|
17
20
|
export const TODO_ACTIVATION_RISK_LIMIT = 20;
|
|
18
21
|
function activationFailure(message) {
|
|
19
22
|
reject({
|
|
@@ -25,7 +28,7 @@ function activationFailure(message) {
|
|
|
25
28
|
export function todoLegacyRowFingerprint(section, line) {
|
|
26
29
|
return createHash("sha256").update(section).update("\0").update(line).digest("hex");
|
|
27
30
|
}
|
|
28
|
-
export function todoReconciliationActivationBytes(retainedLegacyRows, effectSha256, effectOperation = "activate") {
|
|
31
|
+
export function todoReconciliationActivationBytes(retainedLegacyRows, effectSha256, effectOperation = "activate", ownerMappingSha256) {
|
|
29
32
|
const retained = [...retainedLegacyRows].sort();
|
|
30
33
|
if (retained.length > TODO_RECONCILIATION_ITEM_LIMIT
|
|
31
34
|
|| new Set(retained).size !== retained.length
|
|
@@ -33,11 +36,18 @@ export function todoReconciliationActivationBytes(retainedLegacyRows, effectSha2
|
|
|
33
36
|
activationFailure("TODO reconciliation activation has invalid retained legacy-row identities");
|
|
34
37
|
if (effectSha256 !== undefined && !SHA256.test(effectSha256))
|
|
35
38
|
activationFailure("TODO reconciliation activation has an invalid effect authorization");
|
|
39
|
+
if (ownerMappingSha256 !== undefined && !SHA256.test(ownerMappingSha256))
|
|
40
|
+
activationFailure("TODO reconciliation activation has an invalid owner-mapping authorization");
|
|
41
|
+
if (effectOperation === "correct-owners" && (effectSha256 === undefined || ownerMappingSha256 === undefined))
|
|
42
|
+
activationFailure("TODO owner correction activation requires effect and owner-mapping authorization");
|
|
43
|
+
if (effectOperation !== "correct-owners" && ownerMappingSha256 !== undefined)
|
|
44
|
+
activationFailure("TODO reconciliation activation owner-mapping authorization requires owner correction");
|
|
36
45
|
return `${JSON.stringify({
|
|
37
46
|
schema_version: TODO_RECONCILIATION_ACTIVATION_VERSION,
|
|
38
47
|
retained_legacy_rows: retained,
|
|
39
48
|
...(effectSha256 ? { effect_sha256: effectSha256 } : {}),
|
|
40
49
|
...(effectSha256 ? { effect_operation: effectOperation } : {}),
|
|
50
|
+
...(ownerMappingSha256 ? { owner_mapping_sha256: ownerMappingSha256 } : {}),
|
|
41
51
|
})}\n`;
|
|
42
52
|
}
|
|
43
53
|
export function loadTodoReconciliationActivation(root) {
|
|
@@ -63,14 +73,17 @@ export function loadTodoReconciliationActivation(root) {
|
|
|
63
73
|
activationFailure("TODO reconciliation activation is not a mapping");
|
|
64
74
|
}
|
|
65
75
|
const record = value;
|
|
66
|
-
if (!["retained_legacy_rows,schema_version", "effect_operation,effect_sha256,retained_legacy_rows,schema_version", "effect_sha256,retained_legacy_rows,schema_version"].includes(Object.keys(record).sort().join(","))
|
|
76
|
+
if (!["retained_legacy_rows,schema_version", "effect_operation,effect_sha256,retained_legacy_rows,schema_version", "effect_operation,effect_sha256,owner_mapping_sha256,retained_legacy_rows,schema_version", "effect_sha256,retained_legacy_rows,schema_version"].includes(Object.keys(record).sort().join(","))
|
|
67
77
|
|| record.schema_version !== TODO_RECONCILIATION_ACTIVATION_VERSION
|
|
68
78
|
|| !Array.isArray(record.retained_legacy_rows)
|
|
69
79
|
|| record.retained_legacy_rows.length > TODO_RECONCILIATION_ITEM_LIMIT
|
|
70
80
|
|| record.retained_legacy_rows.some((digest) => typeof digest !== "string" || !SHA256.test(digest))
|
|
71
81
|
|| new Set(record.retained_legacy_rows).size !== record.retained_legacy_rows.length
|
|
72
82
|
|| (record.effect_sha256 !== undefined && (typeof record.effect_sha256 !== "string" || !SHA256.test(record.effect_sha256)))
|
|
73
|
-
|| (record.effect_operation !== undefined && !["activate", "repair"].includes(record.effect_operation))
|
|
83
|
+
|| (record.effect_operation !== undefined && !["activate", "repair", "correct-owners"].includes(record.effect_operation))
|
|
84
|
+
|| (record.owner_mapping_sha256 !== undefined && (typeof record.owner_mapping_sha256 !== "string" || !SHA256.test(record.owner_mapping_sha256)))
|
|
85
|
+
|| (record.effect_operation === "correct-owners" && (record.effect_sha256 === undefined || record.owner_mapping_sha256 === undefined))
|
|
86
|
+
|| (record.effect_operation !== "correct-owners" && record.owner_mapping_sha256 !== undefined))
|
|
74
87
|
activationFailure("TODO reconciliation activation has an invalid canonical record");
|
|
75
88
|
return { record: record, bytes };
|
|
76
89
|
}
|
|
@@ -138,3 +151,32 @@ export function todoRepairEffect(diagnosis, targets, publicPath, markdownBefore,
|
|
|
138
151
|
const authorizedTargets = changedTargets.map((target) => target.path === TODO_RECONCILIATION_ACTIVATION_PATH ? { ...target, after_sha256: normalizedActivationSha256 } : target);
|
|
139
152
|
return { ...evidence, effect_sha256: sha256(canonicalRecordJson({ ...evidence, targets: authorizedTargets })) };
|
|
140
153
|
}
|
|
154
|
+
export function todoOwnerCorrectionEffect(diagnosis, ownerMappingSha256, targets, publicPath, markdownBefore, rendered) {
|
|
155
|
+
const changedTargets = targets
|
|
156
|
+
.filter((target) => target.before === null || !target.before.equals(Buffer.from(target.after)))
|
|
157
|
+
.map((target) => ({ path: target.path, before_sha256: target.before === null ? null : sha256(target.before), after_sha256: sha256(target.after) }));
|
|
158
|
+
const evidence = {
|
|
159
|
+
diagnosis,
|
|
160
|
+
owner_mapping_sha256: ownerMappingSha256,
|
|
161
|
+
targets: changedTargets,
|
|
162
|
+
public_document: {
|
|
163
|
+
path: publicPath,
|
|
164
|
+
changed: !markdownBefore.equals(Buffer.from(rendered)),
|
|
165
|
+
before_bytes: markdownBefore.length,
|
|
166
|
+
after_bytes: Buffer.byteLength(rendered),
|
|
167
|
+
before_sha256: sha256(markdownBefore),
|
|
168
|
+
after_sha256: sha256(rendered),
|
|
169
|
+
changed_lines: boundedPublicChanges(markdownBefore.toString("utf8"), rendered),
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const activationTarget = targets.find((target) => target.path === TODO_RECONCILIATION_ACTIVATION_PATH);
|
|
173
|
+
let normalizedActivationSha256 = null;
|
|
174
|
+
if (activationTarget) {
|
|
175
|
+
const parsed = JSON.parse(activationTarget.after);
|
|
176
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || typeof parsed.effect_sha256 !== "string")
|
|
177
|
+
throw new Error("TODO owner correction activation target cannot be normalized for effect authorization");
|
|
178
|
+
normalizedActivationSha256 = sha256(canonicalRecordJson({ ...parsed, effect_sha256: "activation_effect_authorization" }));
|
|
179
|
+
}
|
|
180
|
+
const authorizedTargets = changedTargets.map((target) => target.path === TODO_RECONCILIATION_ACTIVATION_PATH ? { ...target, after_sha256: normalizedActivationSha256 } : target);
|
|
181
|
+
return { ...evidence, effect_sha256: sha256(canonicalRecordJson({ ...evidence, targets: authorizedTargets })) };
|
|
182
|
+
}
|
|
@@ -5,7 +5,7 @@ import { renderTodoPublicRecord } from "../cli/todoMarkdown.js";
|
|
|
5
5
|
import { canonicalRecordJson } from "./archiveDiscovery.js";
|
|
6
6
|
import { detectStateMode } from "./stateMode.js";
|
|
7
7
|
import { StateWriteInputError } from "./write/errors.js";
|
|
8
|
-
import { TODO_ACTIVATION_APPLY_COMMAND, TODO_ACTIVATION_PREVIEW_COMMAND, TODO_RECONCILIATION_ACTIVATION_PATH, TODO_REPAIR_APPLY_COMMAND, TODO_REPAIR_PREVIEW_COMMAND, TODO_UNSAFE_INACTIVE_RECOVERY, loadTodoReconciliationActivation, todoReconciliationActivationBytes, } from "./todoReconciliationActivation.js";
|
|
8
|
+
import { TODO_ACTIVATION_APPLY_COMMAND, TODO_ACTIVATION_PREVIEW_COMMAND, TODO_OWNER_CORRECTION_APPLY_COMMAND, TODO_OWNER_CORRECTION_PREVIEW_COMMAND, TODO_RECONCILIATION_ACTIVATION_PATH, TODO_REPAIR_APPLY_COMMAND, TODO_REPAIR_PREVIEW_COMMAND, TODO_UNSAFE_INACTIVE_RECOVERY, loadTodoReconciliationActivation, todoReconciliationActivationBytes, } from "./todoReconciliationActivation.js";
|
|
9
9
|
import { planTodoRepair } from "./todoReconciliationRepair.js";
|
|
10
10
|
import { readTodoMarkdown } from "./todoMarkdownProjection.js";
|
|
11
11
|
import { inactiveTodoActivationSafety } from "./todoActivationSafety.js";
|
|
@@ -47,8 +47,8 @@ function invalidLifecycle() {
|
|
|
47
47
|
function inspection(state, rawCounts, risks) {
|
|
48
48
|
const bounded = boundedCounts(rawCounts);
|
|
49
49
|
const active = state === "healthy_active" || state === "unsafe_active";
|
|
50
|
-
const preview = state === "inactive" ? TODO_ACTIVATION_PREVIEW_COMMAND : active ? TODO_REPAIR_PREVIEW_COMMAND : null;
|
|
51
|
-
const apply = state === "inactive" ? TODO_ACTIVATION_APPLY_COMMAND : active ? TODO_REPAIR_APPLY_COMMAND : null;
|
|
50
|
+
const preview = state === "unsafe_inactive" ? TODO_OWNER_CORRECTION_PREVIEW_COMMAND : state === "inactive" ? TODO_ACTIVATION_PREVIEW_COMMAND : active ? TODO_REPAIR_PREVIEW_COMMAND : null;
|
|
51
|
+
const apply = state === "unsafe_inactive" ? TODO_OWNER_CORRECTION_APPLY_COMMAND : state === "inactive" ? TODO_ACTIVATION_APPLY_COMMAND : active ? TODO_REPAIR_APPLY_COMMAND : null;
|
|
52
52
|
return {
|
|
53
53
|
state,
|
|
54
54
|
status: state === "healthy_active" ? "operable" : "action_required",
|
|
@@ -106,6 +106,8 @@ export function inspectTodoReconciliationState(root, sourceRoot = resolveSourceR
|
|
|
106
106
|
if (!activation) {
|
|
107
107
|
try {
|
|
108
108
|
const scan = managedRows(readTodoMarkdown(todoPublicPath(root, sourceRoot)).text, null, entities);
|
|
109
|
+
if (entities.length === 0 && scan.matchedRows === 0 && scan.retainedLegacyRows.length === 0)
|
|
110
|
+
return null;
|
|
109
111
|
const safety = inactiveTodoActivationSafety(scan, entities);
|
|
110
112
|
return inspection(safety.safe ? "inactive" : "unsafe_inactive", safety.counts, safety.safe ? undefined : safety.risks);
|
|
111
113
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { parseTodoMarkdownListItem, renderTodoPublicRecord } from "../cli/todoMarkdown.js";
|
|
3
|
+
import { canonicalRecordJson } from "./archiveDiscovery.js";
|
|
2
4
|
import { reject } from "./write/errors.js";
|
|
3
|
-
import { todoLegacyRowFingerprint, TODO_REPAIR_PREVIEW_COMMAND } from "./todoReconciliationActivation.js";
|
|
5
|
+
import { todoLegacyRowFingerprint, TODO_OWNER_CORRECTION_INPUT_VERSION, TODO_OWNER_CORRECTION_PREVIEW_COMMAND, TODO_REPAIR_PREVIEW_COMMAND } from "./todoReconciliationActivation.js";
|
|
4
6
|
const RECONCILIATION_VERSION = "agentera.todoReconciliation.v1";
|
|
5
7
|
const DIAGNOSTIC_LIMIT = 20;
|
|
6
8
|
function mapping(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
@@ -30,6 +32,64 @@ function importMarkdown(record, row) {
|
|
|
30
32
|
return result;
|
|
31
33
|
}
|
|
32
34
|
function rowFor(id, record) { return `- [${record.status === "resolved" ? "x" : " "}] [id:${id}] ${renderTodoPublicRecord(record)}`; }
|
|
35
|
+
export function todoOwnerCorrectionInputViolations(input) {
|
|
36
|
+
const violations = [];
|
|
37
|
+
const allowed = new Set(["schema_version", "owners"]);
|
|
38
|
+
for (const key of Object.keys(input))
|
|
39
|
+
if (!allowed.has(key))
|
|
40
|
+
violations.push(`input field '${key}' is not accepted for owner correction`);
|
|
41
|
+
if (input.schema_version !== TODO_OWNER_CORRECTION_INPUT_VERSION)
|
|
42
|
+
violations.push(`schema_version must be '${TODO_OWNER_CORRECTION_INPUT_VERSION}'`);
|
|
43
|
+
if (!Array.isArray(input.owners)) {
|
|
44
|
+
violations.push("owners must be a list of owner mappings");
|
|
45
|
+
return violations;
|
|
46
|
+
}
|
|
47
|
+
if (input.owners.length === 0 || input.owners.length > 256)
|
|
48
|
+
violations.push("owners must contain 1..256 mappings");
|
|
49
|
+
const ids = new Set();
|
|
50
|
+
const lines = new Set();
|
|
51
|
+
input.owners.forEach((owner, index) => {
|
|
52
|
+
if (!mapping(owner)) {
|
|
53
|
+
violations.push(`owners[${index}] must be a mapping`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const keys = Object.keys(owner).sort().join(",");
|
|
57
|
+
if (keys !== "id,source_line")
|
|
58
|
+
violations.push(`owners[${index}] must contain only id and source_line`);
|
|
59
|
+
if (typeof owner.id !== "string" || !/^[a-z]{10}$/.test(owner.id))
|
|
60
|
+
violations.push(`owners[${index}].id must be a bare ten-letter TODO ID`);
|
|
61
|
+
else if (ids.has(owner.id))
|
|
62
|
+
violations.push(`owners[${index}].id duplicates another owner claim`);
|
|
63
|
+
else
|
|
64
|
+
ids.add(owner.id);
|
|
65
|
+
if (!Number.isSafeInteger(owner.source_line) || Number(owner.source_line) < 1)
|
|
66
|
+
violations.push(`owners[${index}].source_line must be a positive integer`);
|
|
67
|
+
else if (lines.has(Number(owner.source_line)))
|
|
68
|
+
violations.push(`owners[${index}].source_line duplicates another owner claim`);
|
|
69
|
+
else
|
|
70
|
+
lines.add(Number(owner.source_line));
|
|
71
|
+
});
|
|
72
|
+
return violations.length <= DIAGNOSTIC_LIMIT
|
|
73
|
+
? violations
|
|
74
|
+
: [...violations.slice(0, DIAGNOSTIC_LIMIT), `${violations.length - DIAGNOSTIC_LIMIT} additional input violations omitted`];
|
|
75
|
+
}
|
|
76
|
+
export function normalizeTodoOwnerCorrectionEvidence(input) {
|
|
77
|
+
const violations = todoOwnerCorrectionInputViolations(input);
|
|
78
|
+
if (violations.length)
|
|
79
|
+
reject({
|
|
80
|
+
class: "schema_violation",
|
|
81
|
+
message: "TODO owner correction input is invalid",
|
|
82
|
+
violations,
|
|
83
|
+
recovery: `Run exactly '${TODO_OWNER_CORRECTION_PREVIEW_COMMAND}' with one complete schema-valid owner mapping; no state was changed.`,
|
|
84
|
+
});
|
|
85
|
+
const owners = input.owners
|
|
86
|
+
.map((owner) => ({ id: String(owner.id), source_line: Number(owner.source_line) }))
|
|
87
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
88
|
+
return {
|
|
89
|
+
owners,
|
|
90
|
+
sha256: createHash("sha256").update(canonicalRecordJson({ schema_version: TODO_OWNER_CORRECTION_INPUT_VERSION, owners })).digest("hex"),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
33
93
|
function scanRows(markdown) {
|
|
34
94
|
const managed = new Map();
|
|
35
95
|
const legacy = [];
|
|
@@ -168,3 +228,91 @@ export function planTodoRepair(markdown, activation, entities) {
|
|
|
168
228
|
const retainedLegacyRows = retainedRows.filter((row) => !claimedLegacy.has(row.line)).map((row) => todoLegacyRowFingerprint(row.section, row.sourceLine)).sort();
|
|
169
229
|
return { records, retainedLegacyRows, rendered, diagnosis: { counts: { duplicate, stale, matched, retained: retainedLegacyRows.length, conflicting: 0 }, items, omitted_count: Math.max(0, duplicate + matched + retainedLegacyRows.length - items.length) } };
|
|
170
230
|
}
|
|
231
|
+
function correctedRow(row, id, record) {
|
|
232
|
+
const indent = row.sourceLine.match(/^\s*/)?.[0] ?? "";
|
|
233
|
+
return `${indent}${rowFor(id, record)}`;
|
|
234
|
+
}
|
|
235
|
+
export function planTodoOwnerCorrection(markdown, entities, evidence) {
|
|
236
|
+
const { managed, legacy } = scanRows(markdown);
|
|
237
|
+
const rows = new Map();
|
|
238
|
+
for (const row of legacy)
|
|
239
|
+
rows.set(row.line, row);
|
|
240
|
+
for (const entries of managed.values())
|
|
241
|
+
for (const row of entries)
|
|
242
|
+
rows.set(row.line, row);
|
|
243
|
+
const byId = new Map(entities.filter((entity) => Boolean(entity.id && entity.record)).map((entity) => [entity.id, entity]));
|
|
244
|
+
const conflicts = [];
|
|
245
|
+
const claimedIds = new Set();
|
|
246
|
+
const claimedLines = new Set();
|
|
247
|
+
const records = new Map();
|
|
248
|
+
const replacements = new Map();
|
|
249
|
+
const items = [];
|
|
250
|
+
let stale = 0;
|
|
251
|
+
for (const owner of evidence.owners) {
|
|
252
|
+
const entity = byId.get(owner.id);
|
|
253
|
+
const row = rows.get(owner.source_line - 1);
|
|
254
|
+
if (!entity) {
|
|
255
|
+
conflicts.push(`owner ID '${owner.id}' has no canonical entity`);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (!row) {
|
|
259
|
+
conflicts.push(`owner source line ${owner.source_line} has no managed TODO row`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (row.item.id && row.item.id !== owner.id) {
|
|
263
|
+
conflicts.push(`owner source line ${owner.source_line} already names '${row.item.id}', not '${owner.id}'`);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (!claimedIds.add(owner.id) || !claimedLines.add(row.line)) {
|
|
267
|
+
conflicts.push(`owner mapping duplicates ID '${owner.id}' or source line ${owner.source_line}`);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (entity.record.status === "resolved" && row.item.status === "open") {
|
|
271
|
+
conflicts.push(`owner source line ${owner.source_line} would reopen completed TODO '${owner.id}'`);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const record = importMarkdown(entity.record, row);
|
|
275
|
+
const stalePublic = !samePublic(publicSnapshot(entity.record), rowSnapshot(row, entity.record));
|
|
276
|
+
if (stalePublic)
|
|
277
|
+
stale += 1;
|
|
278
|
+
records.set(owner.id, record);
|
|
279
|
+
replacements.set(row.line, correctedRow(row, owner.id, record));
|
|
280
|
+
if (items.length < DIAGNOSTIC_LIMIT)
|
|
281
|
+
items.push({ id: owner.id, source_line: owner.source_line, stale_public: stalePublic });
|
|
282
|
+
}
|
|
283
|
+
for (const id of byId.keys())
|
|
284
|
+
if (!claimedIds.has(id))
|
|
285
|
+
conflicts.push(`canonical TODO entity '${id}' has no owner mapping`);
|
|
286
|
+
for (const [line, row] of rows)
|
|
287
|
+
if (!claimedLines.has(line))
|
|
288
|
+
conflicts.push(`managed TODO row at line ${row.line + 1} has no owner mapping`);
|
|
289
|
+
for (const [id, entries] of managed) {
|
|
290
|
+
if (entries.length > 1)
|
|
291
|
+
conflicts.push(`managed ID '${id}' occurs at lines ${entries.map((row) => row.line + 1).join(", ")}`);
|
|
292
|
+
if (!byId.has(id))
|
|
293
|
+
conflicts.push(`managed ID '${id}' has no canonical entity`);
|
|
294
|
+
}
|
|
295
|
+
if (conflicts.length)
|
|
296
|
+
reject({
|
|
297
|
+
class: "conflict",
|
|
298
|
+
message: `TODO owner correction requires complete one-to-one evidence; found ${conflicts.length} conflicting claim${conflicts.length === 1 ? "" : "s"}`,
|
|
299
|
+
violations: [...conflicts.slice(0, DIAGNOSTIC_LIMIT), ...(conflicts.length > DIAGNOSTIC_LIMIT ? [`${conflicts.length - DIAGNOSTIC_LIMIT} additional conflicts omitted`] : [])],
|
|
300
|
+
diagnosis: {
|
|
301
|
+
counts: { matched: records.size, converted: legacy.length, retained: 0, duplicate: 0, stale, conflicting: conflicts.length },
|
|
302
|
+
items,
|
|
303
|
+
omitted_count: Math.max(0, evidence.owners.length - items.length),
|
|
304
|
+
conflicts_omitted_count: Math.max(0, conflicts.length - DIAGNOSTIC_LIMIT),
|
|
305
|
+
},
|
|
306
|
+
recovery: `Correct every id/source_line claim and rerun exactly '${TODO_OWNER_CORRECTION_PREVIEW_COMMAND}'; no state was changed.`,
|
|
307
|
+
});
|
|
308
|
+
const rendered = `${markdown.split(/\r?\n/).map((line, index) => replacements.get(index) ?? line).join("\n").replace(/\n*$/, "")}\n`;
|
|
309
|
+
return {
|
|
310
|
+
records,
|
|
311
|
+
rendered,
|
|
312
|
+
diagnosis: {
|
|
313
|
+
counts: { matched: records.size, converted: legacy.length, retained: 0, duplicate: 0, stale, conflicting: 0 },
|
|
314
|
+
items,
|
|
315
|
+
omitted_count: Math.max(0, evidence.owners.length - items.length),
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
@@ -99,7 +99,7 @@ function parseJournal(bytes, fileName) {
|
|
|
99
99
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
100
100
|
invalidJournal("TODO reconciliation journal is not a mapping");
|
|
101
101
|
const value = parsed;
|
|
102
|
-
const expectedKeys = ["schema_version", "id", "public_path", "mapping_sha256", "targets", ...(value.create === undefined ? [] : ["create"]), ...(value.activation_effect_sha256 === undefined ? [] : ["activation_effect_sha256"])].sort().join(",");
|
|
102
|
+
const expectedKeys = ["schema_version", "id", "public_path", "mapping_sha256", "targets", ...(value.create === undefined ? [] : ["create"]), ...(value.activation_effect_sha256 === undefined ? [] : ["activation_effect_sha256"]), ...(value.owner_mapping_sha256 === undefined ? [] : ["owner_mapping_sha256"])].sort().join(",");
|
|
103
103
|
if (Object.keys(value).sort().join(",") !== expectedKeys
|
|
104
104
|
|| value.schema_version !== VERSION
|
|
105
105
|
|| typeof value.id !== "string"
|
|
@@ -113,6 +113,8 @@ function parseJournal(bytes, fileName) {
|
|
|
113
113
|
invalidJournal("TODO reconciliation journal is malformed");
|
|
114
114
|
if (value.activation_effect_sha256 !== undefined && !/^[a-f0-9]{64}$/.test(value.activation_effect_sha256))
|
|
115
115
|
invalidJournal("TODO reconciliation journal has an invalid activation effect authorization");
|
|
116
|
+
if (value.owner_mapping_sha256 !== undefined && !/^[a-f0-9]{64}$/.test(value.owner_mapping_sha256))
|
|
117
|
+
invalidJournal("TODO reconciliation journal has an invalid owner-mapping authorization");
|
|
116
118
|
if (value.create !== undefined && (!value.create
|
|
117
119
|
|| typeof value.create !== "object"
|
|
118
120
|
|| Array.isArray(value.create)
|
|
@@ -140,15 +142,17 @@ function parseJournal(bytes, fileName) {
|
|
|
140
142
|
}
|
|
141
143
|
const body = value.targets;
|
|
142
144
|
const activates = body.some((target) => target.path === TODO_RECONCILIATION_ACTIVATION_PATH);
|
|
143
|
-
if (!activates && value.activation_effect_sha256 !== undefined)
|
|
145
|
+
if (!activates && (value.activation_effect_sha256 !== undefined || value.owner_mapping_sha256 !== undefined))
|
|
144
146
|
invalidJournal("TODO reconciliation journal effect authorization has no activation target");
|
|
147
|
+
if (value.owner_mapping_sha256 !== undefined && value.activation_effect_sha256 === undefined)
|
|
148
|
+
invalidJournal("TODO reconciliation journal owner-mapping authorization has no effect authorization");
|
|
145
149
|
const inferredCreate = createReceiptFromTargets(body);
|
|
146
150
|
if (value.create && (!inferredCreate
|
|
147
151
|
|| value.create.created_id !== inferredCreate.created_id
|
|
148
152
|
|| value.create.request_sha256 !== inferredCreate.request_sha256))
|
|
149
153
|
invalidJournal("TODO reconciliation create receipt does not match its canonical entity target");
|
|
150
|
-
const identity = value.create || value.activation_effect_sha256
|
|
151
|
-
? { ...(value.create ? { create: value.create } : {}), ...(value.activation_effect_sha256 ? { activation_effect_sha256: value.activation_effect_sha256 } : {}), targets: body }
|
|
154
|
+
const identity = value.create || value.activation_effect_sha256 || value.owner_mapping_sha256
|
|
155
|
+
? { ...(value.create ? { create: value.create } : {}), ...(value.activation_effect_sha256 ? { activation_effect_sha256: value.activation_effect_sha256 } : {}), ...(value.owner_mapping_sha256 ? { owner_mapping_sha256: value.owner_mapping_sha256 } : {}), targets: body }
|
|
152
156
|
: body;
|
|
153
157
|
const expectedId = createHash("sha256").update(canonicalRecordJson(identity)).digest("hex").slice(0, 24);
|
|
154
158
|
if (value.id !== expectedId || (fileName !== undefined && fileName !== `${value.id}.json`)) {
|
|
@@ -454,6 +458,12 @@ export function recoverTodoReconciliation(context, sourceRoot, binding, options
|
|
|
454
458
|
message: "pending TODO activation does not match the authorized preview effect",
|
|
455
459
|
recovery: "Retry the exact activation apply command returned by the original preview; no transaction target bytes were changed.",
|
|
456
460
|
});
|
|
461
|
+
if (journal.owner_mapping_sha256 && options.ownerMappingSha256 !== journal.owner_mapping_sha256)
|
|
462
|
+
reject({
|
|
463
|
+
class: "conflict",
|
|
464
|
+
message: "pending TODO owner correction does not match the supplied owner mapping",
|
|
465
|
+
recovery: "Retry the exact correction input and apply command returned by the original preview; no transaction target bytes were changed.",
|
|
466
|
+
});
|
|
457
467
|
options.beforeRecovery?.(journal.targets.map((target) => ({
|
|
458
468
|
path: target.path,
|
|
459
469
|
before: target.before === null ? null : decode(target.before),
|
|
@@ -498,10 +508,12 @@ export function publishTodoReconciliation(context, sourceRoot, binding, targets,
|
|
|
498
508
|
|| Number(left.path === binding.publicPath) - Number(right.path === binding.publicPath)
|
|
499
509
|
|| left.path.localeCompare(right.path));
|
|
500
510
|
const body = normalized.map((target) => ({ path: target.path, before: target.before === null ? null : encode(target.before), after: encode(target.after) }));
|
|
501
|
-
if (options.activationEffectSha256 && !normalized.some((target) => target.path === TODO_RECONCILIATION_ACTIVATION_PATH))
|
|
511
|
+
if ((options.activationEffectSha256 || options.ownerMappingSha256) && !normalized.some((target) => target.path === TODO_RECONCILIATION_ACTIVATION_PATH))
|
|
502
512
|
reject({ class: "schema_violation", message: "TODO activation authorization requires its activation target", recovery: "Recompute the complete activation target set from a fresh dry-run; no state was changed." });
|
|
503
|
-
|
|
504
|
-
|
|
513
|
+
if (options.ownerMappingSha256 && !options.activationEffectSha256)
|
|
514
|
+
reject({ class: "schema_violation", message: "TODO owner-mapping authorization requires its effect authorization", recovery: "Recompute the complete owner correction target set from a fresh dry-run; no state was changed." });
|
|
515
|
+
const identity = options.create || options.activationEffectSha256 || options.ownerMappingSha256
|
|
516
|
+
? { ...(options.create ? { create: options.create } : {}), ...(options.activationEffectSha256 ? { activation_effect_sha256: options.activationEffectSha256 } : {}), ...(options.ownerMappingSha256 ? { owner_mapping_sha256: options.ownerMappingSha256 } : {}), targets: body }
|
|
505
517
|
: body;
|
|
506
518
|
const id = createHash("sha256").update(canonicalRecordJson(identity)).digest("hex").slice(0, 24);
|
|
507
519
|
if (!normalized.length)
|
|
@@ -533,6 +545,7 @@ export function publishTodoReconciliation(context, sourceRoot, binding, targets,
|
|
|
533
545
|
mapping_sha256: binding.mappingSha256,
|
|
534
546
|
...(options.create ? { create: options.create } : {}),
|
|
535
547
|
...(options.activationEffectSha256 ? { activation_effect_sha256: options.activationEffectSha256 } : {}),
|
|
548
|
+
...(options.ownerMappingSha256 ? { owner_mapping_sha256: options.ownerMappingSha256 } : {}),
|
|
536
549
|
targets: body,
|
|
537
550
|
};
|
|
538
551
|
const bytes = `${JSON.stringify(journal)}\n`;
|