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.
- package/README.md +7 -3
- package/bundle/.cursor/agents/agentera.md +1 -1
- package/bundle/.opencode/agents/agentera.md +1 -1
- package/bundle/CHANGELOG.md +11 -0
- package/bundle/references/analysis/evidence-tier-authority.yaml +516 -0
- package/bundle/references/artifacts/state-storage-authority.yaml +109 -3
- package/bundle/references/artifacts/verbosity-budget-authority.yaml +42 -0
- package/bundle/references/cli/agent-ready-state-contract.yaml +22 -14
- package/bundle/references/cli/prime-consumer-compatibility.yaml +676 -0
- package/bundle/references/cli/vocabulary-index.yaml +3 -3
- package/bundle/references/cli/vocabulary.md +10 -3
- package/bundle/skills/agentera/SKILL.md +7 -6
- package/bundle/skills/agentera/agents/audit.toml +1 -1
- package/bundle/skills/agentera/agents/build.toml +1 -1
- package/bundle/skills/agentera/agents/design.toml +1 -1
- package/bundle/skills/agentera/agents/discuss.toml +1 -1
- package/bundle/skills/agentera/agents/document.toml +1 -1
- package/bundle/skills/agentera/agents/optimize.toml +1 -1
- package/bundle/skills/agentera/agents/orchestrate.toml +1 -1
- package/bundle/skills/agentera/agents/plan.toml +1 -1
- package/bundle/skills/agentera/agents/profile.toml +1 -1
- package/bundle/skills/agentera/agents/research.toml +1 -1
- package/bundle/skills/agentera/agents/status.toml +1 -1
- package/bundle/skills/agentera/agents/vision.toml +1 -1
- package/bundle/skills/agentera/capabilities/profile/schemas/validation.yaml +31 -3
- package/bundle/skills/agentera/schemas/artifacts/changelog.yaml +1 -0
- package/bundle/skills/agentera/schemas/artifacts/design.yaml +1 -0
- package/bundle/skills/agentera/schemas/artifacts/todo.yaml +1 -0
- package/dist/analytics/extractCorpus/cli.js +33 -5
- package/dist/analytics/extractCorpus/cli.js.map +1 -1
- package/dist/analytics/extractCorpus/evidenceTiers.js +561 -0
- package/dist/analytics/extractCorpus/evidenceTiers.js.map +1 -0
- package/dist/analytics/extractCorpus/extractCorpusParity.js +1 -0
- package/dist/analytics/extractCorpus/extractCorpusParity.js.map +1 -1
- package/dist/analytics/extractCorpus/index.js +3 -0
- package/dist/analytics/extractCorpus/index.js.map +1 -1
- package/dist/analytics/extractCorpus/tierReader.js +109 -0
- package/dist/analytics/extractCorpus/tierReader.js.map +1 -0
- package/dist/analytics/profileSignals.js +212 -0
- package/dist/analytics/profileSignals.js.map +1 -0
- package/dist/analytics/usageStats.js +104 -20
- package/dist/analytics/usageStats.js.map +1 -1
- package/dist/capabilities/index.js +2 -1
- package/dist/capabilities/index.js.map +1 -1
- package/dist/capabilities/profile/instructions.js +1 -1
- package/dist/capabilities/profile/instructions.js.map +1 -1
- package/dist/capabilities/status/startupInstructions.js +35 -0
- package/dist/capabilities/status/startupInstructions.js.map +1 -0
- package/dist/cli/capabilityContext/build.js +3 -3
- package/dist/cli/capabilityContext/build.js.map +1 -1
- package/dist/cli/capabilityContext/closeout.js +5 -5
- package/dist/cli/capabilityContext/closeout.js.map +1 -1
- package/dist/cli/capabilityContext/contract.js +4 -4
- package/dist/cli/capabilityContext/contract.js.map +1 -1
- package/dist/cli/capabilityContext/evidence.js +4 -4
- package/dist/cli/capabilityContext/evidence.js.map +1 -1
- package/dist/cli/capabilityContext/planState.js +7 -6
- package/dist/cli/capabilityContext/planState.js.map +1 -1
- package/dist/cli/capabilityContext/shared.js +1 -1
- package/dist/cli/capabilityContext/shared.js.map +1 -1
- package/dist/cli/capabilityContext/startup.js +4 -4
- package/dist/cli/capabilityContext/startup.js.map +1 -1
- package/dist/cli/commands/lint.js +10 -2
- package/dist/cli/commands/lint.js.map +1 -1
- package/dist/cli/commands/prime/briefOrientation.js +582 -0
- package/dist/cli/commands/prime/briefOrientation.js.map +1 -0
- package/dist/cli/commands/prime/collectOrientationState.js +3 -0
- package/dist/cli/commands/prime/collectOrientationState.js.map +1 -1
- package/dist/cli/commands/prime/orientationOutput.js +117 -23
- package/dist/cli/commands/prime/orientationOutput.js.map +1 -1
- package/dist/cli/commands/prime.js +30 -3
- package/dist/cli/commands/prime.js.map +1 -1
- package/dist/cli/commands/report.js +55 -15
- package/dist/cli/commands/report.js.map +1 -1
- package/dist/cli/commands/schema.js +3 -29
- package/dist/cli/commands/schema.js.map +1 -1
- package/dist/cli/commands/state/plan.js +3 -2
- package/dist/cli/commands/state/plan.js.map +1 -1
- package/dist/cli/help.js +1 -1
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/orientation/corpusCoverage.js +32 -0
- package/dist/cli/orientation/corpusCoverage.js.map +1 -1
- package/dist/cli/orientation.js +3 -3
- package/dist/cli/orientation.js.map +1 -1
- package/dist/cli/startupCompletenessContract.js +3 -3
- package/dist/cli/startupCompletenessContract.js.map +1 -1
- package/dist/cli/stateQuery.js +37 -0
- package/dist/cli/stateQuery.js.map +1 -1
- package/dist/registries/evidenceTierContract.js +362 -0
- package/dist/registries/evidenceTierContract.js.map +1 -0
- package/dist/registries/verbosityBudgetContract.js +206 -0
- package/dist/registries/verbosityBudgetContract.js.map +1 -0
- package/dist/state/decisionLegacyValidation.js +185 -0
- package/dist/state/decisionLegacyValidation.js.map +1 -0
- package/dist/state/decisionOverlay.js +24 -1
- package/dist/state/decisionOverlay.js.map +1 -1
- package/dist/state/decisionRevision.js +622 -0
- package/dist/state/decisionRevision.js.map +1 -0
- package/dist/state/decisionRevisionPublication.js +532 -0
- package/dist/state/decisionRevisionPublication.js.map +1 -0
- package/dist/state/directRetrieval.js +55 -4
- package/dist/state/directRetrieval.js.map +1 -1
- package/dist/state/listRetrieval.js +42 -11
- package/dist/state/listRetrieval.js.map +1 -1
- package/dist/state/startupAnalysis/benchmark.js +70 -8
- package/dist/state/startupAnalysis/benchmark.js.map +1 -1
- package/dist/state/startupAnalysis/threshold.js +13 -0
- package/dist/state/startupAnalysis/threshold.js.map +1 -1
- package/dist/state/startupProjection.js +68 -1
- package/dist/state/startupProjection.js.map +1 -1
- package/dist/state/write/candidateMutation.js +331 -0
- package/dist/state/write/candidateMutation.js.map +1 -0
- package/dist/state/write/explain.js +36 -12
- package/dist/state/write/explain.js.map +1 -1
- package/dist/state/write/helpers.js +22 -0
- package/dist/state/write/helpers.js.map +1 -1
- package/dist/state/write/mutation.js +42 -0
- package/dist/state/write/mutation.js.map +1 -1
- package/dist/state/write/operations.js +49 -1
- package/dist/state/write/operations.js.map +1 -1
- package/dist/state/write/transaction.js +23 -357
- package/dist/state/write/transaction.js.map +1 -1
- package/dist/validate/lifecycleAdapters/lifecycleMain.js +154 -0
- package/dist/validate/lifecycleAdapters/lifecycleMain.js.map +1 -0
- package/dist/validate/lifecycleAdapters.js +5 -156
- package/dist/validate/lifecycleAdapters.js.map +1 -1
- package/dist/validate/selfAudit.js +38 -43
- package/dist/validate/selfAudit.js.map +1 -1
- 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
|