📄 CorpusManager.sol
D-OPEN SOVEREIGN
Documentation & Insights
CorpusManager — Multi-sig Corpus Version Control
Manages collection corpus versions with approval workflows.
Each collection has a curator group that must approve versions before publication.
Supports versioning, rollback, and multi-language corpus tiers.
Specification: packages/specification_doc/corpus_manager_contract.md1// SPDX-License-Identifier: Apache-2.0
2pragma solidity ^0.8.24;
3
4/**
5 * CorpusManager — Multi-sig Corpus Version Control
6 *
7 * Manages collection corpus versions with approval workflows.
8 * Each collection has a curator group that must approve versions before publication.
9 * Supports versioning, rollback, and multi-language corpus tiers.
10 *
11 * Specification: packages/specification_doc/corpus_manager_contract.md
12 */
13
14import "./ErrorLibrary.sol";
15
16contract CorpusManager {
17
18 // ─── Version Structure ───────────────────────────────────────────────────
19
20 enum VersionStatus { DRAFT, APPROVED, PUBLISHED, DEPRECATED }
21
22 struct FileTier {
23 string tierName; // "slim", "extended", "prov"
24 string arweaveTxId; // Arweave TX ID of .bin file
25 uint256 recordCount;
26 uint256 byteLength;
27 string contentHash; // SHA256 for integrity verification
28 }
29
30 struct CorpusVersion {
31 uint256 versionId;
32 string collectionId;
33
34 // Manifest (one per language)
35 mapping(string => string) manifestTxIds; // lang → TX ID
36
37 // File tiers (one per tier per language)
38 mapping(string => FileTier[]) fileTiers; // lang → [slim, ext, prov]
39 string[] languages; // tracked separately for iteration
40
41 // Metadata
42 address publishedBy;
43 uint256 publishedAt;
44 string commitMessage;
45 uint256 parentVersionId;
46
47 VersionStatus status;
48 }
49
50 struct ApprovalProposal {
51 uint256 proposalId;
52 uint256 versionId;
53 string collectionId;
54
55 address proposedBy;
56 uint256 proposedAt;
57
58 address[] curators;
59 uint256 requiredSignatures;
60 mapping(address => bool) approved;
61 uint256 approvalCount;
62
63 bool executed;
64 string rejectionReason;
65 }
66
67 struct CuratorGroup {
68 string collectionId;
69 address[] members;
70 uint256 requiredApprovals;
71 address collectionOwner;
72 }
73
74 // ─── State ──────────────────────────────────────────────────────────────
75
76 mapping(string => CorpusVersion[]) public versions; // collectionId → versions
77 mapping(string => uint256) public currentVersionId; // collectionId → active version
78 mapping(string => CuratorGroup) public curatorGroups; // collectionId → curators
79 mapping(uint256 => ApprovalProposal) public proposals; // proposalId → proposal
80
81 uint256 public nextProposalId = 1;
82 uint256 public nextVersionId = 1;
83
84 // LibraryRegistry reference (optional, for validation)
85 address public libraryRegistry;
86
87 // ─── Events ─────────────────────────────────────────────────────────────
88
89 event ProposalCreated(
90 uint256 indexed proposalId,
91 string indexed collectionId,
92 address proposedBy,
93 uint256 versionId
94 );
95
96 event ApprovalAdded(
97 uint256 indexed proposalId,
98 address approver,
99 uint256 approvalCount,
100 uint256 required
101 );
102
103 event ProposalApproved(
104 uint256 indexed proposalId,
105 string indexed collectionId
106 );
107
108 event VersionPublished(
109 string indexed collectionId,
110 uint256 versionId,
111 uint256 publishedAt
112 );
113
114 event VersionRolledBack(
115 string indexed collectionId,
116 uint256 fromVersionId,
117 uint256 toVersionId
118 );
119
120 event CuratorGroupUpdated(
121 string indexed collectionId,
122 address[] curators,
123 uint256 requiredApprovals
124 );
125
126 // ─── Modifiers ──────────────────────────────────────────────────────────
127
128 modifier onlyCollectionOwner(string calldata _collectionId) {
129 require(
130 msg.sender == curatorGroups[_collectionId].collectionOwner,
131 "CorpusManager: not collection owner"
132 );
133 _;
134 }
135
136 modifier onlyProposer(string calldata _collectionId) {
137 CuratorGroup storage group = curatorGroups[_collectionId];
138 bool isMember = false;
139 for (uint256 i = 0; i < group.members.length; i++) {
140 if (group.members[i] == msg.sender) {
141 isMember = true;
142 break;
143 }
144 }
145 require(isMember, "CorpusManager: not a curator");
146 _;
147 }
148
149 modifier onlyApprover(string calldata _collectionId) {
150 CuratorGroup storage group = curatorGroups[_collectionId];
151 bool isMember = false;
152 for (uint256 i = 0; i < group.members.length; i++) {
153 if (group.members[i] == msg.sender) {
154 isMember = true;
155 break;
156 }
157 }
158 require(isMember, "CorpusManager: not a curator");
159 _;
160 }
161
162 // ─── Setup ──────────────────────────────────────────────────────────────
163
164 constructor(address _libraryRegistry) {
165 libraryRegistry = _libraryRegistry;
166 }
167
168 function setCuratorGroup(
169 string calldata _collectionId,
170 address[] calldata _curators,
171 uint256 _requiredApprovals,
172 address _owner
173 ) external onlyCollectionOwner(_collectionId) {
174 require(_curators.length > 0, "CorpusManager: empty curator list");
175 require(
176 _requiredApprovals > 0 && _requiredApprovals <= _curators.length,
177 "CorpusManager: invalid approval threshold"
178 );
179
180 CuratorGroup storage group = curatorGroups[_collectionId];
181 group.collectionId = _collectionId;
182 group.members = _curators;
183 group.requiredApprovals = _requiredApprovals;
184 group.collectionOwner = _owner;
185
186 emit CuratorGroupUpdated(_collectionId, _curators, _requiredApprovals);
187 }
188
189 // ─── Version Management ──────────────────────────────────────────────────
190
191 function proposeVersion(
192 string calldata _collectionId,
193 string[] calldata _languages,
194 string[] calldata _manifestTxIds,
195 FileTier[] calldata _fileTiers,
196 uint256 _parentVersionId,
197 string calldata _commitMessage
198 ) external onlyProposer(_collectionId) returns (uint256 proposalId) {
199 require(_languages.length > 0, "CorpusManager: empty languages");
200 require(
201 _languages.length == _manifestTxIds.length,
202 "CorpusManager: language/manifest mismatch"
203 );
204
205 // Validate parent version
206 if (_parentVersionId > 0) {
207 require(
208 _parentVersionId < versions[_collectionId].length,
209 "CorpusManager: invalid parent version"
210 );
211 }
212
213 // Create new version (stored by appending)
214 uint256 versionId = nextVersionId++;
215 CorpusVersion storage newVer = versions[_collectionId].push();
216 newVer.versionId = versionId;
217 newVer.collectionId = _collectionId;
218 newVer.languages = _languages;
219 newVer.publishedBy = msg.sender;
220 newVer.publishedAt = 0; // not yet published
221 newVer.commitMessage = _commitMessage;
222 newVer.parentVersionId = _parentVersionId;
223 newVer.status = VersionStatus.DRAFT;
224
225 // Store manifest TX IDs per language
226 for (uint256 i = 0; i < _languages.length; i++) {
227 newVer.manifestTxIds[_languages[i]] = _manifestTxIds[i];
228 }
229
230 // Store file tiers (simplified: all tiers stored together)
231 // In production, use nested mapping or array per language
232 for (uint256 i = 0; i < _fileTiers.length; i++) {
233 newVer.fileTiers[_languages[0]].push(_fileTiers[i]);
234 }
235
236 // Create approval proposal
237 proposalId = nextProposalId++;
238 ApprovalProposal storage proposal = proposals[proposalId];
239 proposal.proposalId = proposalId;
240 proposal.versionId = versionId;
241 proposal.collectionId = _collectionId;
242 proposal.proposedBy = msg.sender;
243 proposal.proposedAt = block.timestamp;
244 proposal.curators = curatorGroups[_collectionId].members;
245 proposal.requiredSignatures = curatorGroups[_collectionId].requiredApprovals;
246 proposal.approvalCount = 0;
247 proposal.executed = false;
248
249 emit ProposalCreated(proposalId, _collectionId, msg.sender, versionId);
250 return proposalId;
251 }
252
253 function approveVersion(uint256 _proposalId) external {
254 ApprovalProposal storage proposal = proposals[_proposalId];
255
256 require(!proposal.executed, "CorpusManager: proposal already executed");
257 require(!proposal.approved[msg.sender], "CorpusManager: already approved by sender");
258
259 // Verify caller is a curator
260 bool isCurator = false;
261 for (uint256 i = 0; i < proposal.curators.length; i++) {
262 if (proposal.curators[i] == msg.sender) {
263 isCurator = true;
264 break;
265 }
266 }
267 require(isCurator, "CorpusManager: not authorized to approve");
268
269 // Record approval
270 proposal.approved[msg.sender] = true;
271 proposal.approvalCount++;
272
273 emit ApprovalAdded(
274 _proposalId,
275 msg.sender,
276 proposal.approvalCount,
277 proposal.requiredSignatures
278 );
279
280 // If threshold reached, mark as approved
281 if (proposal.approvalCount >= proposal.requiredSignatures) {
282 emit ProposalApproved(_proposalId, proposal.collectionId);
283 }
284 }
285
286 function publishVersion(uint256 _proposalId) external {
287 ApprovalProposal storage proposal = proposals[_proposalId];
288
289 require(!proposal.executed, "CorpusManager: already published");
290 require(
291 proposal.approvalCount >= proposal.requiredSignatures,
292 "CorpusManager: insufficient approvals"
293 );
294
295 proposal.executed = true;
296
297 CorpusVersion storage ver = versions[proposal.collectionId][
298 findVersionIndex(proposal.collectionId, proposal.versionId)
299 ];
300 ver.status = VersionStatus.PUBLISHED;
301 ver.publishedAt = block.timestamp;
302
303 // Deprecate previous version
304 uint256 prevId = currentVersionId[proposal.collectionId];
305 if (prevId > 0) {
306 CorpusVersion storage prev = versions[proposal.collectionId][
307 findVersionIndex(proposal.collectionId, prevId)
308 ];
309 prev.status = VersionStatus.DEPRECATED;
310 }
311
312 currentVersionId[proposal.collectionId] = proposal.versionId;
313
314 emit VersionPublished(proposal.collectionId, proposal.versionId, block.timestamp);
315 }
316
317 function rollbackToPreviousVersion(string calldata _collectionId)
318 external
319 onlyCollectionOwner(_collectionId)
320 {
321 uint256 currentId = currentVersionId[_collectionId];
322 require(currentId > 0, "CorpusManager: no current version");
323
324 CorpusVersion storage current = versions[_collectionId][
325 findVersionIndex(_collectionId, currentId)
326 ];
327
328 uint256 parentId = current.parentVersionId;
329 require(parentId > 0, "CorpusManager: no parent version to rollback to");
330
331 CorpusVersion storage parent = versions[_collectionId][
332 findVersionIndex(_collectionId, parentId)
333 ];
334
335 current.status = VersionStatus.DEPRECATED;
336 parent.status = VersionStatus.PUBLISHED;
337
338 currentVersionId[_collectionId] = parentId;
339
340 emit VersionRolledBack(_collectionId, currentId, parentId);
341 }
342
343 // ─── Queries ────────────────────────────────────────────────────────────
344
345 function getVersionCount(string calldata _collectionId)
346 external view returns (uint256)
347 {
348 return versions[_collectionId].length;
349 }
350
351 function getCurrentVersionId(string calldata _collectionId)
352 external view returns (uint256)
353 {
354 return currentVersionId[_collectionId];
355 }
356
357 function getVersion(string calldata _collectionId, uint256 _versionId)
358 external view returns (
359 uint256 versionId,
360 string memory commitMessage,
361 address publishedBy,
362 uint256 publishedAt,
363 VersionStatus status
364 )
365 {
366 uint256 idx = findVersionIndex(_collectionId, _versionId);
367 CorpusVersion storage ver = versions[_collectionId][idx];
368 return (
369 ver.versionId,
370 ver.commitMessage,
371 ver.publishedBy,
372 ver.publishedAt,
373 ver.status
374 );
375 }
376
377 function getManifestTxId(string calldata _collectionId, string calldata _lang)
378 external view returns (string memory)
379 {
380 uint256 curId = currentVersionId[_collectionId];
381 require(curId > 0, "CorpusManager: no current version");
382
383 CorpusVersion storage ver = versions[_collectionId][
384 findVersionIndex(_collectionId, curId)
385 ];
386 return ver.manifestTxIds[_lang];
387 }
388
389 function getCuratorGroup(string calldata _collectionId)
390 external view returns (
391 address[] memory members,
392 uint256 requiredApprovals,
393 address owner
394 )
395 {
396 CuratorGroup storage group = curatorGroups[_collectionId];
397 return (group.members, group.requiredApprovals, group.collectionOwner);
398 }
399
400 function getProposal(uint256 _proposalId)
401 external view returns (
402 uint256 versionId,
403 string memory collectionId,
404 address proposedBy,
405 uint256 approvalCount,
406 uint256 required,
407 bool executed
408 )
409 {
410 ApprovalProposal storage p = proposals[_proposalId];
411 return (
412 p.versionId,
413 p.collectionId,
414 p.proposedBy,
415 p.approvalCount,
416 p.requiredSignatures,
417 p.executed
418 );
419 }
420
421 // ─── Helpers ─────────────────────────────────────────────────────────────
422
423 function findVersionIndex(string memory _collectionId, uint256 _versionId)
424 internal view returns (uint256)
425 {
426 CorpusVersion[] storage vers = versions[_collectionId];
427 for (uint256 i = 0; i < vers.length; i++) {
428 if (vers[i].versionId == _versionId) return i;
429 }
430 revert("CorpusManager: version not found");
431 }
432}
433