📄 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.md
1// 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