agentera 3.0.0-dev.38 → 3.0.0-dev.39

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.
Files changed (101) hide show
  1. package/bundle/CHANGELOG.md +29 -4
  2. package/bundle/references/artifacts/state-storage-authority.yaml +118 -77
  3. package/bundle/references/cli/agent-ready-state-contract.yaml +84 -2
  4. package/bundle/skills/agentera/SKILL.md +26 -26
  5. package/bundle/skills/agentera/capabilities/build/schemas/exit.yaml +5 -4
  6. package/bundle/skills/agentera/capabilities/build/schemas/validation.yaml +17 -3
  7. package/bundle/skills/agentera/schemas/artifacts/plan.yaml +14 -3
  8. package/dist/capabilities/build/instructions.js +27 -2
  9. package/dist/capabilities/build/instructions.js.map +1 -1
  10. package/dist/capabilities/orchestrate/instructions.js +1 -1
  11. package/dist/cli/capabilityContext/bespoke.js +4 -3
  12. package/dist/cli/capabilityContext/bespoke.js.map +1 -1
  13. package/dist/cli/capabilityContext/build.js +77 -24
  14. package/dist/cli/capabilityContext/build.js.map +1 -1
  15. package/dist/cli/capabilityContext/closeout.js +2 -2
  16. package/dist/cli/capabilityContext/closeout.js.map +1 -1
  17. package/dist/cli/capabilityContext/evidence.js +2 -6
  18. package/dist/cli/capabilityContext/evidence.js.map +1 -1
  19. package/dist/cli/capabilityContext/planState.js +59 -78
  20. package/dist/cli/capabilityContext/planState.js.map +1 -1
  21. package/dist/cli/capabilityContext/projectVerification.js +410 -0
  22. package/dist/cli/capabilityContext/projectVerification.js.map +1 -0
  23. package/dist/cli/capabilityContext/startup.js +2 -2
  24. package/dist/cli/capabilityContext/startup.js.map +1 -1
  25. package/dist/cli/commands/prime/buildExecutionRequest.js +165 -0
  26. package/dist/cli/commands/prime/buildExecutionRequest.js.map +1 -0
  27. package/dist/cli/commands/prime/collectEntityOrientation.js +2 -1
  28. package/dist/cli/commands/prime/collectEntityOrientation.js.map +1 -1
  29. package/dist/cli/commands/prime/collectOrientationState.js +3 -1
  30. package/dist/cli/commands/prime/collectOrientationState.js.map +1 -1
  31. package/dist/cli/commands/prime.js +37 -2
  32. package/dist/cli/commands/prime.js.map +1 -1
  33. package/dist/cli/commands/query.js +4 -0
  34. package/dist/cli/commands/query.js.map +1 -1
  35. package/dist/cli/dispatch/prime.js +22 -1
  36. package/dist/cli/dispatch/prime.js.map +1 -1
  37. package/dist/cli/dispatch/shared.js +1 -1
  38. package/dist/cli/dispatch/shared.js.map +1 -1
  39. package/dist/cli/help.js +2 -1
  40. package/dist/cli/help.js.map +1 -1
  41. package/dist/cli/orientation.js +2 -2
  42. package/dist/cli/orientation.js.map +1 -1
  43. package/dist/registries/glossaryCaveatContract.js +3 -1
  44. package/dist/registries/glossaryCaveatContract.js.map +1 -1
  45. package/dist/state/changelog.js +222 -0
  46. package/dist/state/changelog.js.map +1 -0
  47. package/dist/state/decisionEntities.js +16 -9
  48. package/dist/state/decisionEntities.js.map +1 -1
  49. package/dist/state/decisionMigrationProvenance.js +84 -1
  50. package/dist/state/decisionMigrationProvenance.js.map +1 -1
  51. package/dist/state/decisionOverlay.js +9 -32
  52. package/dist/state/decisionOverlay.js.map +1 -1
  53. package/dist/state/decisionRevision.js +69 -444
  54. package/dist/state/decisionRevision.js.map +1 -1
  55. package/dist/state/decisionRevisionMigration.js +90 -0
  56. package/dist/state/decisionRevisionMigration.js.map +1 -0
  57. package/dist/state/directRetrieval.js +3 -52
  58. package/dist/state/directRetrieval.js.map +1 -1
  59. package/dist/state/entityCutover.js +6 -6
  60. package/dist/state/entityCutover.js.map +1 -1
  61. package/dist/state/entityGlossaryAuthority.js +14 -0
  62. package/dist/state/entityGlossaryAuthority.js.map +1 -0
  63. package/dist/state/entityMigrationIdentity.js +6 -0
  64. package/dist/state/entityMigrationIdentity.js.map +1 -0
  65. package/dist/state/entityMigrationPreview.js +14 -23
  66. package/dist/state/entityMigrationPreview.js.map +1 -1
  67. package/dist/state/entityStorage.js +20 -19
  68. package/dist/state/entityStorage.js.map +1 -1
  69. package/dist/state/healthEntities.js +2 -4
  70. package/dist/state/healthEntities.js.map +1 -1
  71. package/dist/state/listRetrieval.js +7 -38
  72. package/dist/state/listRetrieval.js.map +1 -1
  73. package/dist/state/migrationSourceBinding.js +1 -1
  74. package/dist/state/migrationSourceBinding.js.map +1 -1
  75. package/dist/state/objectiveExperimentEntities.js +3 -3
  76. package/dist/state/objectiveExperimentEntities.js.map +1 -1
  77. package/dist/state/planEntities.js +40 -7
  78. package/dist/state/planEntities.js.map +1 -1
  79. package/dist/state/progressEntities.js +2 -4
  80. package/dist/state/progressEntities.js.map +1 -1
  81. package/dist/state/safeProjectFile.js +4 -1
  82. package/dist/state/safeProjectFile.js.map +1 -1
  83. package/dist/state/startupProjection.js +2 -70
  84. package/dist/state/startupProjection.js.map +1 -1
  85. package/dist/state/stateStorageAuthority.js +48 -0
  86. package/dist/state/stateStorageAuthority.js.map +1 -0
  87. package/dist/state/summarySourceRowAuthority.js +1 -1
  88. package/dist/state/summarySourceRowAuthority.js.map +1 -1
  89. package/dist/state/todoDocsEntities.js +2 -3
  90. package/dist/state/todoDocsEntities.js.map +1 -1
  91. package/dist/state/write/explain.js +38 -59
  92. package/dist/state/write/explain.js.map +1 -1
  93. package/dist/state/write/operations.js +2 -26
  94. package/dist/state/write/operations.js.map +1 -1
  95. package/dist/state/write/transaction.js +3 -3
  96. package/dist/state/write/transaction.js.map +1 -1
  97. package/dist/upgrade/upgradeOrchestrator.js +15 -10
  98. package/dist/upgrade/upgradeOrchestrator.js.map +1 -1
  99. package/package.json +2 -2
  100. package/dist/state/decisionRevisionPublication.js +0 -532
  101. package/dist/state/decisionRevisionPublication.js.map +0 -1
@@ -1,31 +1,6 @@
1
- import { createHash } from "node:crypto";
2
- import fs from "node:fs";
3
1
  import path from "node:path";
4
- import { loadYamlMapping } from "../core/yaml.js";
5
2
  import { resolveSourceRoot } from "../core/sourceRoot.js";
6
- import { assertRealpathBoundary } from "../registries/artifactRegistry.js";
7
- import { canonicalRecordJson, readNumberedArchiveEntry, stateCurrentProjectionPath, validateStateRecord, } from "./archiveDiscovery.js";
8
- import { legacyEntryNumber } from "./legacyIdentity.js";
9
- import { reject } from "./write/errors.js";
10
- // partitionDecisionViolations lives in decisionLegacyValidation.js, which in
11
- // turn imports the coexistence primitives declared below. Both modules
12
- // reference each other only inside functions, so the circular binding is
13
- // resolved by the time either is called (same pattern as the
14
- // decisionRevisionPublication.js re-export at the end of this file).
15
- import { partitionDecisionViolations } from "./decisionLegacyValidation.js";
16
- /**
17
- * Focused accessor for the decision-content amendment contract declared in
18
- * references/artifacts/state-storage-authority.yaml (`revisions:` and
19
- * `compatibility.legacy_label_coexistence:`). It reads the same single
20
- * authority file as the other focused accessors (e.g. gitBackfillAuthority) and
21
- * projects only the amendment-relevant sections.
22
- *
23
- * The overlay (satisfaction) and revision (content) authorities share an
24
- * identity prefix but never overlap fields. This module owns the revision
25
- * contract, the revision document loader, base→revisions composition, and
26
- * amendment preparation. Amendment publication with recovery is plan task 3.
27
- */
28
- const AUTHORITY_RELATIVE_PATH = "references/artifacts/state-storage-authority.yaml";
3
+ import { loadStateStorageAuthority } from "./stateStorageAuthority.js";
29
4
  const EXPECTED_AUTHORITY_SCHEMA = "agentera.stateStorageAuthority.v1";
30
5
  function mapping(value) {
31
6
  return value !== null && typeof value === "object" && !Array.isArray(value)
@@ -39,13 +14,14 @@ function requiredString(value, field) {
39
14
  return value;
40
15
  }
41
16
  function requiredList(value, field) {
42
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) {
17
+ if (!Array.isArray(value) ||
18
+ value.some((item) => typeof item !== "string" || item.length === 0)) {
43
19
  throw new Error(`state storage authority field '${field}' must be a list of non-empty strings`);
44
20
  }
45
21
  return value;
46
22
  }
47
23
  function readAuthority(sourceRoot) {
48
- const authority = loadYamlMapping(fs.readFileSync(path.join(sourceRoot, AUTHORITY_RELATIVE_PATH), "utf8"));
24
+ const authority = loadStateStorageAuthority(sourceRoot).document;
49
25
  if (authority.schema_version !== EXPECTED_AUTHORITY_SCHEMA) {
50
26
  throw new Error("state storage authority schema_version is unsupported");
51
27
  }
@@ -54,11 +30,6 @@ function readAuthority(sourceRoot) {
54
30
  }
55
31
  return authority;
56
32
  }
57
- /**
58
- * Cross-validation inputs extracted from the same authority: the decision
59
- * overlay identity prefix and mutable paths. Revisions must share the overlay
60
- * identity and never overlap an overlay mutable path.
61
- */
62
33
  function overlayCrossCheck(authority) {
63
34
  const overlays = mapping(authority.overlays);
64
35
  const identityKey = requiredString(overlays.identity_key, "overlays.identity_key");
@@ -91,23 +62,32 @@ export function decisionRevisionContract(sourceRoot = resolveSourceRoot()) {
91
62
  throw new Error("state storage authority revisions identity_key prefix must match the overlay identity");
92
63
  }
93
64
  const amendablePaths = requiredList(revisions.amendable_paths, "revisions.amendable_paths");
65
+ const legacyAmendablePaths = requiredList(revisions.legacy_amendable_paths, "revisions.legacy_amendable_paths");
66
+ const migrationAmendablePaths = requiredList(revisions.migration_amendable_paths, "revisions.migration_amendable_paths");
94
67
  const identityPaths = requiredList(revisions.identity_paths, "revisions.identity_paths");
95
68
  const temporalPaths = requiredList(revisions.temporal_paths, "revisions.temporal_paths");
96
69
  const reserved = new Set([...identityPaths, ...temporalPaths]);
97
- for (const amendable of amendablePaths) {
98
- if (reserved.has(amendable)) {
99
- throw new Error(`state storage authority revisions.amendable_paths must not include identity or temporal path '${amendable}'`);
100
- }
101
- if (overlay.mutablePaths.some((mutable) => amendable === mutable || amendable.startsWith(`${mutable}.`))) {
102
- throw new Error(`state storage authority revisions.amendable_paths must not overlap overlay mutable path '${amendable}'`);
70
+ for (const [field, paths] of [["amendable_paths", amendablePaths], ["legacy_amendable_paths", legacyAmendablePaths], ["migration_amendable_paths", migrationAmendablePaths]]) {
71
+ for (const amendable of paths) {
72
+ if (reserved.has(amendable)) {
73
+ throw new Error(`state storage authority revisions.${field} must not include identity or temporal path '${amendable}'`);
74
+ }
75
+ if (overlay.mutablePaths.some((mutable) => amendable === mutable || amendable.startsWith(`${mutable}.`))) {
76
+ throw new Error(`state storage authority revisions.${field} must not overlap overlay mutable path '${amendable}'`);
77
+ }
103
78
  }
104
79
  }
105
80
  return {
106
81
  location,
82
+ legacySourceState: requiredString(revisions.legacy_source_state, "revisions.legacy_source_state"),
107
83
  schemaVersion: requiredString(revisions.schema_version, "revisions.schema_version"),
108
84
  identityKey,
109
85
  identityPrefix,
110
86
  amendablePaths,
87
+ legacyAmendablePaths,
88
+ migrationAmendablePaths,
89
+ currentConfidenceVocabulary: requiredList(mapping(mapping(authority.compatibility).legacy_label_coexistence).current_vocabulary, "compatibility.legacy_label_coexistence.current_vocabulary"),
90
+ knownLegacyConfidenceValues: requiredList(mapping(mapping(authority.compatibility).legacy_label_coexistence).known_legacy_examples, "compatibility.legacy_label_coexistence.known_legacy_examples"),
111
91
  identityPaths,
112
92
  temporalPaths,
113
93
  separationFromOverlay: requiredString(revisions.separation_from_overlay, "revisions.separation_from_overlay"),
@@ -138,14 +118,6 @@ export function legacyLabelCoexistence(sourceRoot = resolveSourceRoot()) {
138
118
  noSilentNormalization: requiredString(coexistence.no_silent_normalization, "compatibility.legacy_label_coexistence.no_silent_normalization"),
139
119
  };
140
120
  }
141
- /**
142
- * Evaluate the legacy-label coexistence rule for one confidence value.
143
- * `touched=true` marks the value as new or amended content supplied by append
144
- * or amend; `touched=false` marks an untouched inherited value on an existing
145
- * record. The current vocabulary always passes. An untouched unsupported
146
- * value is explicit legacy state (preserved, caveated, allowed). A touched
147
- * unsupported value is rejected and requires the current vocabulary.
148
- */
149
121
  export function classifyConfidenceLabel(contract, value, touched) {
150
122
  const vocabulary = contract.currentVocabulary.join(", ");
151
123
  if (contract.currentVocabulary.includes(value)) {
@@ -164,12 +136,6 @@ export function classifyConfidenceLabel(contract, value, touched) {
164
136
  caveat: `confidence label '${value}' requires current vocabulary (${vocabulary}) for new or amended content`,
165
137
  };
166
138
  }
167
- /**
168
- * Revision provenance vocabulary (state-storage-authority `revisions.provenance`).
169
- * A revision carries revision provenance only and is never labeled as
170
- * historical archive provenance. A revision whose base was bootstrapped from a
171
- * complete legacy projection may carry degraded_projection provenance.
172
- */
173
139
  export const REVISION_PROVENANCE_VALUES = ["historical_revision", "degraded_projection"];
174
140
  const REVISION_META_KEYS = new Set(["date", "provenance", "base_sha256"]);
175
141
  const FORBIDDEN_REVISION_PROVENANCE = "historical_archive";
@@ -186,28 +152,12 @@ function decisionNumberFromId(id, contract) {
186
152
  const parsed = Number(number);
187
153
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
188
154
  }
189
- /**
190
- * Resolve the revision document path for a project. Mirrors
191
- * {@link decisionOverlayPath}: project-relative, realpath-bounded.
192
- */
193
- export function decisionRevisionPath(projectRoot, sourceRoot = resolveSourceRoot()) {
194
- const contract = decisionRevisionContract(sourceRoot);
195
- const target = path.resolve(projectRoot, contract.location);
196
- assertRealpathBoundary(projectRoot, target, "decision revision");
197
- return target;
198
- }
199
- /**
200
- * Validate a revision document against the amendment contract. Returns a list of
201
- * human-readable violations (empty when valid). The document must be a mapping
202
- * keyed by stable decision ID; each value is an ordered list of revision
203
- * records carrying only amendment-meta keys and amendable content paths.
204
- */
205
155
  export function decisionRevisionViolations(document, contract = decisionRevisionContract()) {
206
156
  if (!isRecord(document)) {
207
157
  return ["decision revision root must be a mapping keyed by stable decision ID"];
208
158
  }
209
- const amendable = new Set(contract.amendablePaths);
210
- const allowed = new Set([...REVISION_META_KEYS, ...contract.amendablePaths]);
159
+ const amendable = new Set(contract.legacyAmendablePaths);
160
+ const allowed = new Set([...REVISION_META_KEYS, ...contract.legacyAmendablePaths]);
211
161
  const violations = [];
212
162
  for (const [stableId, value] of Object.entries(document)) {
213
163
  if (decisionNumberFromId(stableId, contract) === null) {
@@ -219,12 +169,12 @@ export function decisionRevisionViolations(document, contract = decisionRevision
219
169
  continue;
220
170
  }
221
171
  value.forEach((revision, index) => {
222
- violations.push(...decisionRevisionRecordViolations(revision, `${stableId}[${index}]`, contract, allowed, amendable));
172
+ violations.push(...decisionRevisionRecordViolations(revision, `${stableId}[${index}]`, contract, allowed, amendable, contract.legacyAmendablePaths, true));
223
173
  });
224
174
  }
225
175
  return violations;
226
176
  }
227
- function decisionRevisionRecordViolations(revision, label, contract, allowed = new Set([...REVISION_META_KEYS, ...contract.amendablePaths]), amendable = new Set(contract.amendablePaths)) {
177
+ function decisionRevisionRecordViolations(revision, label, contract, allowed = new Set([...REVISION_META_KEYS, ...contract.amendablePaths, ...contract.migrationAmendablePaths]), amendable = new Set([...contract.amendablePaths, ...contract.migrationAmendablePaths]), amendablePaths = [...contract.amendablePaths, ...contract.migrationAmendablePaths], legacy = false, allowLegacyConfidence = false) {
228
178
  if (!isRecord(revision))
229
179
  return [`${label} must be a mapping of amendable fields and revision provenance`];
230
180
  const violations = [];
@@ -232,30 +182,67 @@ function decisionRevisionRecordViolations(revision, label, contract, allowed = n
232
182
  if (!allowed.has(key))
233
183
  violations.push(`${label} carries '${key}' which is not an amendable content path or revision meta key`);
234
184
  }
185
+ if ("alternatives" in revision && ("alternatives.chosen" in revision || "alternatives.rejected" in revision)) {
186
+ violations.push(`${label} must not mix migration-only alternatives replacement with granular alternative changes`);
187
+ }
188
+ for (const key of amendable) {
189
+ if (!(key in revision))
190
+ continue;
191
+ const value = revision[key];
192
+ if (key === "alternatives") {
193
+ const alternatives = Array.isArray(value) ? value : [];
194
+ const malformed = !Array.isArray(value)
195
+ || alternatives.length === 0
196
+ || alternatives.some((entry) => {
197
+ const candidate = isRecord(entry) ? entry : {};
198
+ return typeof candidate.name !== "string"
199
+ || candidate.name.length === 0
200
+ || !["chosen", "rejected"].includes(String(candidate.status));
201
+ })
202
+ || alternatives.filter((entry) => isRecord(entry) && entry.status === "chosen").length !== 1;
203
+ if (malformed) {
204
+ violations.push(`${label}.alternatives must be a non-empty list with exactly one named chosen alternative and zero or more named rejected alternatives`);
205
+ }
206
+ }
207
+ else if (key === "alternatives.rejected") {
208
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.length === 0)) {
209
+ violations.push(`${label}.alternatives.rejected must be a list of non-empty strings`);
210
+ }
211
+ }
212
+ else if (typeof value !== "string" || value.length === 0) {
213
+ violations.push(`${label}.${key} must be a non-empty string`);
214
+ }
215
+ else if (key === "confidence" && !legacy && !contract.currentConfidenceVocabulary.includes(value) && !(allowLegacyConfidence && contract.knownLegacyConfidenceValues.includes(value))) {
216
+ violations.push(`${label}.confidence '${value}' is not one of: ${contract.currentConfidenceVocabulary.join(", ")}`);
217
+ }
218
+ }
235
219
  const provenance = revision.provenance;
236
220
  if (provenance !== undefined) {
237
221
  if (typeof provenance !== "string")
238
222
  violations.push(`${label}.provenance must be a string`);
239
223
  else if (provenance === FORBIDDEN_REVISION_PROVENANCE)
240
224
  violations.push(`${label}.provenance must never claim historical_archive provenance`);
241
- else if (!REVISION_PROVENANCE_VALUES.includes(provenance))
225
+ else if (!REVISION_PROVENANCE_VALUES.includes(provenance)) {
242
226
  violations.push(`${label}.provenance '${provenance}' is not one of: ${REVISION_PROVENANCE_VALUES.join(", ")}`);
227
+ }
243
228
  }
244
229
  const baseSha = revision.base_sha256;
245
- if (baseSha !== undefined && (typeof baseSha !== "string" || !/^[0-9a-f]{64}$/.test(baseSha)))
230
+ if (baseSha !== undefined && (typeof baseSha !== "string" || !/^[0-9a-f]{64}$/.test(baseSha))) {
246
231
  violations.push(`${label}.base_sha256 must be a 64-character sha256 hex digest`);
232
+ }
247
233
  const date = revision.date;
248
- if (date !== undefined && (typeof date !== "string" || date.length === 0))
234
+ if (date !== undefined && (typeof date !== "string" || date.length === 0)) {
249
235
  violations.push(`${label}.date must be a non-empty string`);
250
- if (!Object.keys(revision).some((key) => amendable.has(key)))
251
- violations.push(`${label} must amend at least one content field (${contract.amendablePaths.join(", ")})`);
236
+ }
237
+ if (!Object.keys(revision).some((key) => amendable.has(key))) {
238
+ violations.push(`${label} must amend at least one content field (${amendablePaths.join(", ")})`);
239
+ }
252
240
  return violations;
253
241
  }
254
- /** Validate one canonical entity revision through the same revision contract as legacy revisions. */
255
- export function decisionRevisionEntityViolations(record, contract = decisionRevisionContract()) {
242
+ export function decisionRevisionEntityViolations(record, contract = decisionRevisionContract(), allowLegacyConfidence = false) {
256
243
  if (!isRecord(record.changes))
257
244
  return ["decision revision changes must be a mapping of amendable fields"];
258
- const amendable = new Set(contract.amendablePaths);
245
+ const amendable = new Set([...contract.amendablePaths, ...contract.migrationAmendablePaths]);
259
246
  const violations = Object.keys(record.changes)
260
247
  .filter((key) => !amendable.has(key))
261
248
  .map((key) => `decision revision changes carries '${key}' which is not an amendable content path`);
@@ -264,369 +251,7 @@ export function decisionRevisionEntityViolations(record, contract = decisionRevi
264
251
  date: record.date,
265
252
  provenance: record.provenance,
266
253
  base_sha256: record.base_sha256,
267
- }, "decision revision", contract));
254
+ }, "decision revision", contract, undefined, undefined, undefined, false, allowLegacyConfidence));
268
255
  return violations;
269
256
  }
270
- export function loadDecisionRevision(projectRoot = process.cwd(), sourceRoot = resolveSourceRoot()) {
271
- const target = decisionRevisionPath(projectRoot, sourceRoot);
272
- if (!fs.existsSync(target))
273
- return {};
274
- const bytes = fs.readFileSync(target, "utf8");
275
- let document;
276
- try {
277
- document = loadYamlMapping(bytes);
278
- }
279
- catch (error) {
280
- throw new Error(`cannot parse decision revision document '${target}': ${error.message}`);
281
- }
282
- const violations = decisionRevisionViolations(document, decisionRevisionContract(sourceRoot));
283
- if (violations.length > 0) {
284
- throw new Error(`decision revision document '${target}' is invalid: ${violations.join("; ")}`);
285
- }
286
- return document;
287
- }
288
- /**
289
- * Compose ordered revisions over an immutable base record. Each revision
290
- * applies only its authority-declared amendable content paths, in declared
291
- * order; no revision overwrites, reorders, or deletes a prior revision. The
292
- * overlay (satisfaction) authority composes its own evidence separately and is
293
- * not touched here.
294
- *
295
- * When `baseSha256` is supplied, the first revision that carries a
296
- * `base_sha256` is checked against it; a mismatch marks `broken_hash` so the
297
- * caller degrades rather than reconstructing inconsistent detail.
298
- */
299
- export function composeDecisionRevision(entry, revisions, options = {}) {
300
- const contract = options.contract ?? decisionRevisionContract();
301
- if (!revisions || revisions.length === 0) {
302
- return { record: structuredClone(entry), applied: false, fields: [], revisions: [], broken_hash: false };
303
- }
304
- const effective = structuredClone(entry);
305
- const touched = new Set();
306
- const provenance = [];
307
- let brokenHash = false;
308
- const expectedBase = options.baseSha256;
309
- for (let index = 0; index < revisions.length; index++) {
310
- const revision = revisions[index];
311
- const fields = [];
312
- for (const amendablePath of contract.amendablePaths) {
313
- if (amendablePath in revision && revision[amendablePath] !== undefined) {
314
- effective[amendablePath] = structuredClone(revision[amendablePath]);
315
- touched.add(amendablePath);
316
- fields.push(amendablePath);
317
- }
318
- }
319
- // The first revision establishes the base lineage; a base_sha256 mismatch
320
- // there means the immutable base drifted after the revision was published.
321
- if (index === 0 && expectedBase !== undefined && typeof revision.base_sha256 === "string" && revision.base_sha256 !== expectedBase) {
322
- brokenHash = true;
323
- }
324
- provenance.push({
325
- index,
326
- ...(typeof revision.date === "string" ? { date: revision.date } : {}),
327
- provenance: (typeof revision.provenance === "string" ? revision.provenance : "historical_revision"),
328
- fields,
329
- ...(typeof revision.base_sha256 === "string" ? { base_sha256: revision.base_sha256 } : {}),
330
- });
331
- }
332
- return {
333
- record: effective,
334
- applied: touched.size > 0,
335
- fields: [...touched],
336
- revisions: provenance,
337
- broken_hash: brokenHash,
338
- };
339
- }
340
- /**
341
- * Load the revision snapshot once for a project. Returns `null` when no
342
- * revision document exists. Throws a plain `Error` (with a descriptive
343
- * message) when the document is unreadable or violates the contract, so
344
- * callers can wrap the failure in their own retrieval envelope.
345
- */
346
- export function loadDecisionRevisionSnapshot(projectRoot, sourceRoot = resolveSourceRoot()) {
347
- const contract = decisionRevisionContract(sourceRoot);
348
- const revisionPath = decisionRevisionPath(projectRoot, sourceRoot);
349
- let document;
350
- try {
351
- document = loadDecisionRevision(projectRoot, sourceRoot);
352
- // loadDecisionRevision returns {} when the document is absent; normalize to null
353
- // so callers short-circuit the snapshot path entirely.
354
- document = Object.keys(document).length > 0 ? document : {};
355
- }
356
- catch (error) {
357
- throw new Error(`cannot read decision revision document: ${error.message}`);
358
- }
359
- if (Object.keys(document).length === 0)
360
- return null;
361
- return { path: revisionPath, document, amendablePaths: contract.amendablePaths, contract };
362
- }
363
- /**
364
- * Bounded composition metadata for one stable ID in a list snapshot. Does not
365
- * hash-verify lineage (that is the exact/get path's responsibility); it reports
366
- * whether any amendable content field is amended and which, for truthful list
367
- * provenance without reconstructing full detail.
368
- */
369
- export function decisionRevisionFields(snapshot, stableId) {
370
- if (!snapshot || !stableId || !snapshot.document[stableId]) {
371
- return { applied: false, fields: [], count: 0 };
372
- }
373
- const revisions = snapshot.document[stableId];
374
- const fields = new Set();
375
- for (const entry of revisions) {
376
- for (const amendable of snapshot.amendablePaths) {
377
- if (amendable in entry && entry[amendable] !== undefined)
378
- fields.add(amendable);
379
- }
380
- }
381
- return { applied: fields.size > 0, fields: [...fields], count: revisions.length };
382
- }
383
- /**
384
- * Compose a bounded list summary's effective record from the base and any
385
- * revisions for the stable ID. Returns the base (cloned) when no revisions
386
- * apply, so list summaries never reconstruct missing detail.
387
- */
388
- export function composedRevisionRecord(snapshot, stableId, base) {
389
- if (!snapshot || !stableId || !snapshot.document[stableId])
390
- return structuredClone(base);
391
- return composeDecisionRevision(base, snapshot.document[stableId], { contract: snapshot.contract }).record;
392
- }
393
- /**
394
- * Locate a complete base record for amendment preparation. A verified numbered
395
- * archive seeds a `historical_archive` base. When the decision has no numbered
396
- * archive but is represented by a complete legacy full projection record, the
397
- * exact projection values seed a hash-verified `degraded_projection` base
398
- * (never historical_archive). Summary-only, conflicting, ambiguous,
399
- * broken-hash, and incomplete targets are refused with a working correction
400
- * rather than reconstructed.
401
- */
402
- export function prepareDecisionAmendment(projectRoot, decisionNumber, requested, options = {}) {
403
- if (!Number.isSafeInteger(decisionNumber) || decisionNumber <= 0) {
404
- reject({
405
- class: "invalid_request",
406
- message: `decision number must be a positive integer; received ${decisionNumber}`,
407
- syntax: "agentera state decisions amend --number N [--question ... --choice ... --reasoning ... --confidence firm ...] [--dry-run] --format json",
408
- example: 'agentera state decisions amend --number 53 --choice "..." --reasoning "..." --confidence firm --dry-run --format json',
409
- recovery: "Supply an existing numbered decision and retry; no files were changed.",
410
- });
411
- }
412
- const sourceRoot = options.sourceRoot ?? resolveSourceRoot();
413
- const contract = options.contract ?? decisionRevisionContract(sourceRoot);
414
- const amendableSet = new Set(contract.amendablePaths);
415
- const requestedFields = Object.keys(requested).filter((key) => amendableSet.has(key) && requested[key] !== undefined);
416
- if (requestedFields.length === 0) {
417
- reject({
418
- class: "missing_argument",
419
- message: "amend requires at least one amendable content field",
420
- syntax: "agentera state decisions amend --number N [--question ... --choice ... --reasoning ... --confidence firm ...] [--dry-run] --format json",
421
- example: 'agentera state decisions amend --number 53 --choice "..." --reasoning "..." --confidence firm --dry-run --format json',
422
- recovery: `Supply at least one of ${contract.amendablePaths.join(", ")}; no files were changed.`,
423
- });
424
- }
425
- // New/amended confidence must be current vocabulary; legacy labels are never normalized.
426
- if (requestedFields.includes("confidence")) {
427
- const value = requested.confidence;
428
- const coexistence = legacyLabelCoexistence(sourceRoot);
429
- const classification = classifyConfidenceLabel(coexistence, String(value), true);
430
- if (!classification.allowed) {
431
- reject({
432
- class: "invalid_choice",
433
- message: classification.caveat,
434
- valid_values: coexistence.currentVocabulary,
435
- syntax: "agentera state decisions amend --number N --confidence firm --format json",
436
- example: "agentera state decisions amend --number 53 --confidence firm --dry-run --format json",
437
- recovery: "Use a current-vocabulary confidence label for new or amended content; no files were changed.",
438
- });
439
- }
440
- }
441
- const lookup = readNumberedArchiveEntry(projectRoot, "decisions", decisionNumber, { sourceRoot });
442
- if (lookup.rejection) {
443
- const reason = lookup.rejection.reason;
444
- const brokenHash = reason === "invalid_envelope" || reason === "read_failure" || reason === "unsafe_path";
445
- reject({
446
- class: "conflict",
447
- message: `decision ${decisionNumber} amendment target has a broken numbered archive (${reason}: ${lookup.rejection.message})`,
448
- syntax: "agentera state decisions amend --number N [--question ... --confidence firm ...] [--dry-run] --format json",
449
- example: 'agentera state decisions amend --number 53 --choice "..." --reasoning "..." --confidence firm --dry-run --format json',
450
- recovery: brokenHash
451
- ? "Preserve the affected archive bytes for diagnostics, repair the numbered record, then retry the amend."
452
- : "Restore or publish the validated numbered archive for this ID before retrying; missing fields are not reconstructed.",
453
- });
454
- }
455
- const projectionPath = stateCurrentProjectionPath(projectRoot, "decisions", sourceRoot);
456
- let base;
457
- let currentRepresentation;
458
- if (lookup.entry) {
459
- // A verified numbered archive seeds an immutable historical_archive base.
460
- if (!lookup.entry.record) {
461
- reject({
462
- class: "conflict",
463
- message: `decision ${decisionNumber} amendment target has a numbered archive without a complete record`,
464
- syntax: "agentera state decisions amend --number N [--question ... --confidence firm ...] [--dry-run] --format json",
465
- example: 'agentera state decisions amend --number 53 --choice "..." --reasoning "..." --confidence firm --dry-run --format json',
466
- recovery: "Preserve the numbered archive for diagnostics, repair or republish its complete record, then retry the amend.",
467
- });
468
- }
469
- base = {
470
- record: lookup.entry.record,
471
- sha256: lookup.entry.recordSha256,
472
- provenance: "historical_archive",
473
- source: "archive",
474
- };
475
- currentRepresentation = "full";
476
- }
477
- else {
478
- // No numbered archive: bootstrap a hash-verified base from a complete legacy
479
- // full projection record. Summary-only targets are refused; missing fields
480
- // are never reconstructed.
481
- const projection = legacyFullProjectionFor(projectRoot, decisionNumber, projectionPath, sourceRoot);
482
- if (projection.kind === "missing" || projection.kind === "absent") {
483
- reject({
484
- class: "unsupported_target",
485
- message: `decision ${decisionNumber} has no numbered archive and is not present in the current projection`,
486
- syntax: "agentera state decisions amend --number N [--question ... --confidence firm ...] [--dry-run] --format json",
487
- example: 'agentera state decisions amend --number 53 --choice "..." --reasoning "..." --confidence firm --dry-run --format json',
488
- recovery: "Append or restore the decision with a complete record (and its numbered archive) before amending; missing fields are not reconstructed.",
489
- });
490
- }
491
- else if (projection.kind === "ambiguous") {
492
- reject({
493
- class: "conflict",
494
- message: `decision ${decisionNumber} is represented by multiple conflicting current projection entries`,
495
- syntax: "agentera state decisions amend --number N [--question ... --confidence firm ...] [--dry-run] --format json",
496
- example: 'agentera state decisions amend --number 53 --choice "..." --reasoning "..." --confidence firm --dry-run --format json',
497
- recovery: "Remove the duplicate identity conflict according to the artifact schema, then retry the amend.",
498
- });
499
- }
500
- else if (projection.kind === "summary") {
501
- reject({
502
- class: "unsupported_target",
503
- message: `decision ${decisionNumber} is represented only by an irrecoverable legacy summary; a base cannot be hash-verified`,
504
- syntax: "agentera state decisions amend --number N [--question ... --confidence firm ...] [--dry-run] --format json",
505
- example: 'agentera state decisions amend --number 53 --choice "..." --reasoning "..." --confidence firm --dry-run --format json',
506
- recovery: "Restore or publish the validated numbered archive for this ID before retrying; missing fields are not reconstructed.",
507
- });
508
- }
509
- else {
510
- // degraded_projection base: hash-verified from exact legacy projection values, never historical_archive.
511
- base = {
512
- record: projection.record,
513
- sha256: sha256(projection.record),
514
- provenance: "degraded_projection",
515
- source: "legacy_full",
516
- };
517
- currentRepresentation = "full";
518
- }
519
- }
520
- const revisionPath = decisionRevisionPath(projectRoot, sourceRoot);
521
- let revisions = [];
522
- try {
523
- const document = loadDecisionRevision(projectRoot, sourceRoot);
524
- revisions = document[`decisions:${decisionNumber}`] ?? [];
525
- }
526
- catch (error) {
527
- reject({
528
- class: "conflict",
529
- message: `decision ${decisionNumber} amendment target has a broken revision document: ${error.message}`,
530
- syntax: "agentera state decisions amend --number N [--question ... --confidence firm ...] [--dry-run] --format json",
531
- example: 'agentera state decisions amend --number 53 --choice "..." --reasoning "..." --confidence firm --dry-run --format json',
532
- recovery: "Preserve the revision document for diagnostics, repair its YAML and amendable content paths, then retry the amend.",
533
- });
534
- }
535
- const composed = composeDecisionRevision(base.record, revisions, { contract, baseSha256: base.sha256 });
536
- if (composed.broken_hash) {
537
- reject({
538
- class: "conflict",
539
- message: `decision ${decisionNumber} base hash no longer matches the recorded revision base_sha256; the immutable base drifted after the revision was published`,
540
- syntax: "agentera state decisions amend --number N [--question ... --confidence firm ...] [--dry-run] --format json",
541
- example: 'agentera state decisions amend --number 53 --choice "..." --reasoning "..." --confidence firm --dry-run --format json',
542
- recovery: "Preserve the immutable archive and existing revisions, reconcile the base drift, then retry the amend.",
543
- });
544
- }
545
- const effective = structuredClone(composed.record);
546
- for (const field of requestedFields) {
547
- effective[field] = structuredClone(requested[field]);
548
- }
549
- const replay = composed.applied &&
550
- requestedFields.every((field) => canonicalRecordJson(effective[field]) === canonicalRecordJson(composed.record[field] ?? effective[field])) &&
551
- requestedFields.length === composed.fields.length &&
552
- composed.fields.every((field) => requestedFields.includes(field));
553
- return {
554
- number: decisionNumber,
555
- base,
556
- revisions,
557
- requested: { ...requested },
558
- effective,
559
- replay,
560
- provenance: {
561
- base: base.provenance,
562
- archive: {
563
- path: lookup.path,
564
- available: Boolean(lookup.entry),
565
- verified: Boolean(lookup.entry),
566
- ...(lookup.entry ? { record_sha256: lookup.entry.recordSha256 } : {}),
567
- },
568
- current_projection: { path: projectionPath, representation: currentRepresentation },
569
- revision_path: revisionPath,
570
- existing_revisions: revisions.length,
571
- amended_fields: requestedFields,
572
- },
573
- };
574
- }
575
- export function legacyFullProjectionFor(projectRoot, decisionNumber, projectionPath, sourceRoot) {
576
- if (!fs.existsSync(projectionPath))
577
- return { kind: "missing" };
578
- let bytes;
579
- try {
580
- bytes = fs.readFileSync(projectionPath, "utf8");
581
- }
582
- catch {
583
- return { kind: "missing" };
584
- }
585
- let document;
586
- try {
587
- document = loadYamlMapping(bytes);
588
- }
589
- catch {
590
- return { kind: "missing" };
591
- }
592
- const active = document.decisions;
593
- if (!Array.isArray(active))
594
- return { kind: "absent" };
595
- let match;
596
- for (const candidate of active) {
597
- if (!isRecord(candidate))
598
- continue;
599
- if (legacyEntryNumber(candidate, "decisions", "number") !== decisionNumber)
600
- continue;
601
- if (match)
602
- return { kind: "ambiguous" };
603
- match = candidate;
604
- }
605
- if (!match) {
606
- return { kind: "absent" };
607
- }
608
- // A complete legacy projection record validates against the decision schema;
609
- // otherwise the entry is summary-only and must not be reconstructed. An
610
- // untouched inherited legacy confidence label is explicit legacy state under
611
- // the authority's coexistence rule: it does not degrade a complete record to
612
- // summary, so the base remains hash-verifiable and amend proceeds (the label
613
- // is reported as a compatibility caveat by the amendment path). Tolerance is
614
- // confidence-only — every other violation still degrades to summary.
615
- try {
616
- const violations = validateStateRecord(sourceRoot, "decisions", match);
617
- if (violations.length === 0)
618
- return { kind: "full", record: match };
619
- const partition = partitionDecisionViolations(violations, { decisions: [match] }, legacyLabelCoexistence(sourceRoot), new Set());
620
- return partition.blocking.length === 0 ? { kind: "full", record: match } : { kind: "summary" };
621
- }
622
- catch {
623
- return { kind: "summary" };
624
- }
625
- }
626
- export function sha256(value) {
627
- return createHash("sha256").update(canonicalRecordJson(value), "utf8").digest("hex");
628
- }
629
- // Re-export the publication primitives for backward-compatible imports.
630
- // The implementation lives in decisionRevisionPublication.ts.
631
- export { buildPublishedRevisionRecord, dispatchDecisionAmendment, findConflictingRevision, findPublishedRevision, projectRevisionOverride, publishDecisionAmendment, } from "./decisionRevisionPublication.js";
632
257
  //# sourceMappingURL=decisionRevision.js.map