agentera 3.0.0-dev.21 → 3.0.0-dev.23

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