citesync 0.1.0__tar.gz
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.
- citesync-0.1.0/LICENSE +201 -0
- citesync-0.1.0/NOTICE +6 -0
- citesync-0.1.0/PKG-INFO +213 -0
- citesync-0.1.0/README.md +184 -0
- citesync-0.1.0/pyproject.toml +142 -0
- citesync-0.1.0/pyproject.toml.orig +122 -0
- citesync-0.1.0/src/citesync/MAPPING_SOURCE +5 -0
- citesync-0.1.0/src/citesync/__init__.py +1 -0
- citesync-0.1.0/src/citesync/check.py +247 -0
- citesync-0.1.0/src/citesync/cli.py +202 -0
- citesync-0.1.0/src/citesync/codecs.py +245 -0
- citesync-0.1.0/src/citesync/config.py +168 -0
- citesync-0.1.0/src/citesync/errors.py +23 -0
- citesync-0.1.0/src/citesync/formats.py +359 -0
- citesync-0.1.0/src/citesync/mapping-local.json +408 -0
- citesync-0.1.0/src/citesync/mapping.py +118 -0
- citesync-0.1.0/src/citesync/mapping.schema.json +70 -0
- citesync-0.1.0/src/citesync/py.typed +0 -0
- citesync-0.1.0/src/citesync/rules.py +153 -0
- citesync-0.1.0/src/citesync/write.py +524 -0
citesync-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 Simon van Lierde
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
citesync-0.1.0/NOTICE
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
citesync
|
|
2
|
+
Copyright 2026 Simon van Lierde
|
|
3
|
+
|
|
4
|
+
src/citesync/mapping-local.json is derived from the CodeMeta crosswalks
|
|
5
|
+
(https://github.com/codemeta/codemeta, Apache License 2.0) by way of
|
|
6
|
+
codemeta-drift (https://github.com/simonvanlierde/codemeta-drift, Apache License 2.0).
|
citesync-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: citesync
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Check that CITATION.cff, codemeta.json, .zenodo.json and the package manifests (pyproject.toml, Cargo.toml, package.json) agree, and rewrite them to match one source file, using the CodeMeta crosswalks
|
|
5
|
+
Keywords: citation,CITATION.cff,codemeta,zenodo,software-metadata,pre-commit
|
|
6
|
+
Author: Simon van Lierde
|
|
7
|
+
Author-email: Simon van Lierde <code@duinlab.nl>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
License-File: NOTICE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Dist: packaging>=26.3
|
|
22
|
+
Requires-Dist: ruamel-yaml>=0.19
|
|
23
|
+
Requires-Dist: tomlkit>=0.15.2
|
|
24
|
+
Requires-Python: >=3.12
|
|
25
|
+
Project-URL: Homepage, https://github.com/simonvanlierde/citesync
|
|
26
|
+
Project-URL: Issues, https://github.com/simonvanlierde/citesync/issues
|
|
27
|
+
Project-URL: Changelog, https://github.com/simonvanlierde/citesync/blob/main/CHANGELOG.md
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# citesync
|
|
31
|
+
|
|
32
|
+
[](https://github.com/simonvanlierde/citesync/actions/workflows/python.yml)
|
|
33
|
+
[](https://pypi.org/project/citesync/)
|
|
34
|
+
[](https://pypi.org/project/citesync/)
|
|
35
|
+
[](https://scorecard.dev/viewer/?uri=github.com/simonvanlierde/citesync)
|
|
36
|
+
[](https://github.com/simonvanlierde/citesync/blob/main/LICENSE)
|
|
37
|
+
|
|
38
|
+
Status: experimental. It may change in ways that break your code, without notice.
|
|
39
|
+
|
|
40
|
+
Questions and bugs: see [CONTRIBUTING.md](https://github.com/simonvanlierde/citesync/blob/main/.github/CONTRIBUTING.md).
|
|
41
|
+
|
|
42
|
+
Software citation metadata: citesync checks that CITATION.cff, codemeta.json, .zenodo.json and the package manifests (pyproject.toml, Cargo.toml, package.json) say the same thing. Where each field sits in each file comes from a mapping of CodeMeta's crosswalks, exported by [codemeta-drift](https://github.com/simonvanlierde/codemeta-drift).
|
|
43
|
+
|
|
44
|
+
## What works now
|
|
45
|
+
|
|
46
|
+
`citesync check` compares eight fields with one file, the source (CITATION.cff by default): title/name, version, authors with ORCID, licence, DOI, repository URL, description and keywords.
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
uv tool install citesync # or run it once with uvx citesync check
|
|
50
|
+
citesync check # exit 0: they agree; 1: an error finding; 2: citesync couldn't run
|
|
51
|
+
citesync check --source pyproject.toml
|
|
52
|
+
citesync check --root path/to/repo # check another directory
|
|
53
|
+
citesync check --format json # or --json; also --format github for workflow annotations
|
|
54
|
+
citesync check -v # also list fields that weren't compared (not-checked)
|
|
55
|
+
citesync check -q # only errors, no summary line
|
|
56
|
+
citesync rules # the rules, with their severity; --format json too
|
|
57
|
+
citesync explain version-mismatch # what a rule checks and how to fix it
|
|
58
|
+
citesync config # the config in use, and the file it came from
|
|
59
|
+
citesync write # rewrite the other files to agree with the source
|
|
60
|
+
citesync write --diff # print what write would change, and change nothing
|
|
61
|
+
citesync --version
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Run from a subdirectory, citesync finds the repo root as ruff does: it walks up to the nearest directory with a citation file or a citesync config, and stops at the git root, so a submodule keeps its own files. File paths in findings are relative to that root.
|
|
65
|
+
|
|
66
|
+
Each finding is one line, `file: severity[rule] field: message`, and a last line counts them:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
.zenodo.json:2:1: error[unreadable] Expecting property name enclosed in double quotes
|
|
70
|
+
codemeta.json: error[version-mismatch] version: '2.0.0', CITATION.cff has '1.0.0'
|
|
71
|
+
2 errors, 0 warnings in 3 files
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The severity is in colour on a terminal. `NO_COLOR` turns colour off, and `FORCE_COLOR` turns it on anywhere (`FORCE_COLOR=0` or `false` turns it off). `NO_COLOR` wins.
|
|
75
|
+
|
|
76
|
+
The rule ids name the field: `name-mismatch`, `version-mismatch`, `author-mismatch`, `license-mismatch`, `doi-mismatch`, `repository-mismatch`, `description-mismatch` and `keywords-mismatch` when a field differs from the source; `keywords-not-subset` for a manifest keyword the citation file doesn't have; `context-mismatch` when codemeta.json's `@context` isn't the configured CodeMeta version; `role-dangling` (warning) for a CodeMeta Role in codemeta.json whose person, named by ORCID, isn't listed; `unreadable` for a file citesync can't read; and `not-checked` (info) for a field that wasn't compared. [docs/rules.md](https://github.com/simonvanlierde/citesync/blob/main/docs/rules.md) explains each one. When the source has no place or no value for a field (pyproject.toml has no DOI), the other files that hold it must agree with each other.
|
|
77
|
+
|
|
78
|
+
A file citesync can't read or parse is an `unreadable` finding, with the parser's message and its line and column when the parser gives them. The other files are still checked against each other, and when the unreadable file is the source, each field it holds is reported as `not-checked`.
|
|
79
|
+
|
|
80
|
+
| Exit | When |
|
|
81
|
+
| ---: | --- |
|
|
82
|
+
| 0 | no error finding |
|
|
83
|
+
| 1 | an error finding, an unreadable file included |
|
|
84
|
+
| 2 | citesync couldn't run: a bad config or option, a missing source file, or an internal error. stdout is empty and stderr has one `citesync: <message>` line |
|
|
85
|
+
|
|
86
|
+
`--format json` prints one document:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"version": 1,
|
|
91
|
+
"tool": { "name": "citesync", "version": "0.1.0" },
|
|
92
|
+
"summary": { "files": 3, "errors": 1, "warnings": 0, "info": 0 },
|
|
93
|
+
"findings": [
|
|
94
|
+
{
|
|
95
|
+
"rule": "version-mismatch",
|
|
96
|
+
"severity": "error",
|
|
97
|
+
"file": "codemeta.json",
|
|
98
|
+
"term": "version",
|
|
99
|
+
"message": "'2.0.0', CITATION.cff has '1.0.0'",
|
|
100
|
+
"help": "Make the file agree with the source, or the other way round if the source is wrong.",
|
|
101
|
+
"url": "https://github.com/simonvanlierde/citesync/blob/main/docs/rules.md#version-mismatch",
|
|
102
|
+
"line": null,
|
|
103
|
+
"column": null
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Findings are sorted by file, rule and field, and positions count from 1. A value longer than 80 characters is cut to 77 and ends in `...` in the message. `file` is null for a field no file holds, and `term` (the CodeMeta term) is null for `unreadable`. JSON always lists every finding: `-q` and `-v` change only the text and GitHub output. [tests/output.schema.json](https://github.com/simonvanlierde/citesync/blob/main/tests/output.schema.json) is the JSON Schema. A minor release may add rule ids and optional fields; ignore what you don't know. Removing or retyping a field bumps `version`, in a major release. `--format github` writes one workflow annotation per finding, titled `citesync <rule>`, and ends with a notice that counts them all, since GitHub shows only the first ten of each level.
|
|
110
|
+
|
|
111
|
+
Spellings are normalised before comparing: SPDX ids, SPDX URLs and Zenodo's lowercase ids; ORCID URLs and bare iDs; Zenodo's `"Family, Given"` and npm or Cargo `"Name <email>"`; DOIs with or without `https://doi.org/`, and a codemeta `identifier` given as a schema.org PropertyValue (`propertyID` `DOI`, or a DOI as its `value` or `url`; other PropertyValues, such as a SWHID, are skipped); repository URLs as npm's hosted-git-info reads them: `git+`, `.git`, `http://`, `www.`, the host's case, npm shorthands (`github:owner/repo`, `owner/repo`), SSH remotes, and a branch, tag or file page on GitHub, GitLab, Codeberg, Gitea or Bitbucket (`…/tree/v1.0`, as Zenodo's GitHub integration writes), with GitHub paths in any case; CFF `name-particle` and `name-suffix`, read as part of the family name; a licence list, read as `A OR B`; PEP 621 url labels as packaging normalises them (`Source Code`, `homepage`); versions as PEP 440 versions (`v1.2.3` = `1.2.3`, `1.0` = `1.0.0`, `0.3.1-rc.1` = `0.3.1rc1`, `1.0.0-dev.1` = `1.0.0.dev1`), while a version that isn't one (`unreleased`) must match exactly. Names and keywords compare case-insensitively, with whitespace collapsed and Unicode in NFC; set `keywords-case-sensitive = true` to compare keywords by case too. Findings show each file's own spelling. Manifests are compared on version, authors' names, licence, repository URL and keywords, not on name or description. npm's one `author` is compared with the first author.
|
|
112
|
+
|
|
113
|
+
By default it reads every citation file present and the release manifests at the repo root: a pyproject.toml without `[build-system]` and a private package.json are skipped, a field in pyproject's `dynamic` is skipped on its own, and a Cargo workspace is read through `[workspace.package]`. The tests run each of the six files as the source against a fixture where all six agree, and CITATION.cff as the source against five real repositories.
|
|
114
|
+
|
|
115
|
+
`citesync write` fixes what `check` reports, and nothing more. `citesync write --diff` prints the same edits as a unified diff and writes nothing; it exits 1 when there is a change, or a file it can't read (listed on stderr), so CI can run it. It writes every citation file that isn't the source, and a manifest only for the fields listed in `write-manifests` (or all of them with `"all"`). A field a file leaves out stays out: a .zenodo.json without a version never gains one. When the source has no value for a field (no DOI in pyproject.toml), write leaves that field alone in the other files. It keeps each file's layout: comments, key order and quoting in CITATION.cff, comments in TOML, and in JSON the key order, indentation, one-line arrays and final newline. Each value is spelled the way that file spells it (Zenodo's `"Family, Given"` and lowercase licence ids, a DOI URL in codemeta.json, where a schema.org PropertyValue keeps its other keys, semver in Cargo.toml and package.json, PEP 440 in pyproject.toml). A value the file can't spell is left as it was, and its finding says why: a compound licence in .zenodo.json, a licence with `AND` or `WITH` in CITATION.cff or codemeta.json (`OR` becomes a list), in Cargo.toml and package.json a version semver has no spelling for (a post-release such as `1.2.3.post1`, an epoch, more than three numbers), and in pyproject.toml a version that isn't PEP 440. An author with only a name, such as an organization (`name` in CITATION.cff and codemeta.json), counts as a family name and keeps it. An author whose name or ORCID matches keeps the rest of their entry, such as an email or affiliation; anyone else gets a new entry. When that would drop an old entry holding more than a name and ORCID, such as after a renaming in a manifest, write leaves that file's authors for you to edit. A Cargo field inherited with `.workspace = true` is left as it is: change it in the workspace's `[workspace.package]`.
|
|
116
|
+
|
|
117
|
+
Values that compare equal stay as written: write leaves `v1.2.3` against `1.2.3`, and `LCA` against `lca` unless `keywords-case-sensitive` is set. A finding whose rule `ignore` lists is not written, and an `unreadable` file is never written. It prints `wrote <file>` for each file it changed (to stderr with `--format json` or `github`, and not at all with `--quiet`), then reports the findings left as `check` does, in the same formats, each with what to do: edit it by hand (with the reason, when write could have written it but didn't), or add the field to `write-manifests`. Exit 0 when no error finding remains, 1 when one does (an unreadable file included), and 2 when citesync couldn't run. Every file is written to a temporary file beside it before any is moved into place, so a failure leaves the files as they were, unless a move itself fails, which leaves the files moved before it written. A symlinked file is written through the link, CRLF line ends stay CRLF, and a file that would not change is not touched.
|
|
118
|
+
|
|
119
|
+
Known limits of the CITATION.cff round trip: an end-of-line comment aligned with spaces after a value (`title: x # c`) is reflowed, a second sequence indented differently from the first is reindented to match it, and a closing `...` and a byte-order mark are dropped.
|
|
120
|
+
|
|
121
|
+
Config goes in the first of `citesync.toml`, `.citesync.toml`, `[tool.citesync]` in pyproject.toml, `[workspace.metadata.citesync]` or `[package.metadata.citesync]` in Cargo.toml, or `"citesync"` in package.json:
|
|
122
|
+
|
|
123
|
+
```toml
|
|
124
|
+
source = "CITATION.cff" # any of the six files
|
|
125
|
+
codemeta-version = "3.0"
|
|
126
|
+
files = ["codemeta.json", ".zenodo.json"] # default: the citation files present
|
|
127
|
+
manifests = ["src/core/package.json"] # replaces the default root manifests
|
|
128
|
+
write-manifests = ["keywords", "license"] # or "all"; the manifest fields write may change
|
|
129
|
+
keywords-case-sensitive = true # default false: keywords compare as names do
|
|
130
|
+
ignore = ["description-mismatch"] # rule ids whose findings to drop
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`write-manifests` names fields by their CodeMeta term, as the JSON `term` does: `name`, `version`, `author`, `license`, `identifier` (the DOI), `codeRepository`, `description` and `keywords`. Rule ids use the plainer `doi` and `repository`.
|
|
134
|
+
|
|
135
|
+
An `ignore` entry that matched no finding prints a warning to stderr. An unknown rule id is a config error, and `unreadable` can't be ignored.
|
|
136
|
+
|
|
137
|
+
Not built yet: creating a citation file listed under `files` that doesn't exist yet.
|
|
138
|
+
|
|
139
|
+
## Pre-commit hook
|
|
140
|
+
|
|
141
|
+
For [prek](https://prek.j178.dev) or [pre-commit](https://pre-commit.com), in `.pre-commit-config.yaml`:
|
|
142
|
+
|
|
143
|
+
```yaml
|
|
144
|
+
repos:
|
|
145
|
+
- repo: https://github.com/simonvanlierde/citesync
|
|
146
|
+
rev: v0.1.0
|
|
147
|
+
hooks:
|
|
148
|
+
- id: citesync
|
|
149
|
+
- id: citesync-write
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The hook runs `citesync check` from the repo root when a commit changes CITATION.cff, codemeta.json, .zenodo.json, pyproject.toml, Cargo.toml, package.json, `citesync.toml` or `.citesync.toml`, at any depth. It always checks every file, not only the staged ones, and fails the commit on an error finding. `citesync-write` runs `citesync write` only when you ask for it: `prek run citesync-write --stage manual --all-files`.
|
|
153
|
+
|
|
154
|
+
## Tested on real repositories
|
|
155
|
+
|
|
156
|
+
The tests copy the metadata files of five public repositories byte for byte (commits in [tests/fixtures/SOURCES.txt](https://github.com/simonvanlierde/citesync/blob/main/tests/fixtures/SOURCES.txt)) and check that citesync reports exactly these findings, with CITATION.cff as the source.
|
|
157
|
+
|
|
158
|
+
| Repository | Config | Findings | Exit | Why |
|
|
159
|
+
| --- | --- | --- | ---: | --- |
|
|
160
|
+
| sourceledger | none | codemeta.json `context-mismatch` | 1 | codemeta.json uses the CodeMeta 4.0 context, and citesync expects 3.0 unless `codemeta-version` says otherwise. pyproject.toml and .zenodo.json agree on every field |
|
|
161
|
+
| credit-matrix | none | codemeta.json `name-mismatch` | 1 | codemeta.json names it "CRediT Matrix", CITATION.cff "CRediT Matrix: drafting CRediT contribution statements with co-authors". The root package.json is private, so it is skipped |
|
|
162
|
+
| credit-matrix | `manifests = ["src/core/package.json"]` | codemeta.json `name-mismatch` | 1 | As above. The package agrees: its version, licence and author match, its keywords are a subset, and its `git+…/credit-matrix.git` repository names the same repository |
|
|
163
|
+
| jsonvet | none | Cargo.toml `keywords-not-subset` | 1 | The workspace keywords include `lint`, and CITATION.cff has `linter`. Only Cargo.toml holds a version, so there is nothing to compare it with |
|
|
164
|
+
| jsonvet | `manifests = ["crates/jsonvet-cli/pyproject.toml"]` | that pyproject.toml `keywords-not-subset`; `not-checked` version (info) | 1 | The same keyword. The list replaces the root manifests and the pyproject version is `dynamic`, so no file holds a version |
|
|
165
|
+
| r9lab | none | codemeta.json `context-mismatch` | 1 | As sourceledger. The root pyproject.toml has no `[build-system]` and the root package.json is private, so no manifest is checked |
|
|
166
|
+
| project-templates | none | `not-checked` version (info) | 0 | CITATION.cff alone, with no version |
|
|
167
|
+
|
|
168
|
+
A made-up set where all six files agree exits 0 with each of them as the source.
|
|
169
|
+
|
|
170
|
+
## Development
|
|
171
|
+
|
|
172
|
+
You need [uv](https://docs.astral.sh/uv/) and [just](https://just.systems).
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
uv sync
|
|
176
|
+
uv tool install prek # once per machine
|
|
177
|
+
prek install # git hooks
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`just` lists the tasks. They mirror what [.github/workflows/](https://github.com/simonvanlierde/citesync/tree/main/.github/workflows) runs.
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
just check # lint, types and tests for every stack here
|
|
184
|
+
just fmt # apply each stack's fixes and formatting
|
|
185
|
+
just hooks # the git hooks, as the checks workflow runs them
|
|
186
|
+
just mutation # mutation-test the comparison code (not in CI)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`just mutation` changes `check.py` and `codecs.py` one small step at a time and fails if the tests
|
|
190
|
+
still pass on a change that `tests/mutation-baseline.txt` does not list. A change that
|
|
191
|
+
cannot alter behaviour goes in that file with the reason. It also fails on a listed
|
|
192
|
+
change the tests now catch, so the list stays true.
|
|
193
|
+
|
|
194
|
+
uv locks only releases at least a week old. If `just check` fails on
|
|
195
|
+
a security fix newer than that, add the package to `exclude-newer-package` under `[tool.uv]` in `pyproject.toml`,
|
|
196
|
+
and remove it a week later ([how](https://github.com/simonvanlierde/project-templates/blob/main/docs/tooling.md#dependencies)).
|
|
197
|
+
|
|
198
|
+
## Releasing
|
|
199
|
+
|
|
200
|
+
To release, add a `## [X.Y.Z]` section to `CHANGELOG.md`, then push the tag `vX.Y.Z`.
|
|
201
|
+
`python-release.yml` publishes to PyPI, then creates the GitHub release with the distributions and their provenance bundle.
|
|
202
|
+
Approve the `pypi` environment when prompted. Do not create the GitHub release by hand.
|
|
203
|
+
If the release job fails after PyPI succeeds, delete any leftover draft with `gh release delete vX.Y.Z`.
|
|
204
|
+
Re-run only the `release` job. A published immutable release needs a new patch version to fix its files.
|
|
205
|
+
|
|
206
|
+
Before the first release, set up the publishing environment
|
|
207
|
+
(`pypi`)
|
|
208
|
+
in Settings > Environments: limit it to tags matching `v*`, and add a required reviewer.
|
|
209
|
+
The release workflows explain the rest in their header comments.
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
See [LICENSE](https://github.com/simonvanlierde/citesync/blob/main/LICENSE) for the license terms.
|
citesync-0.1.0/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# citesync
|
|
2
|
+
|
|
3
|
+
[](https://github.com/simonvanlierde/citesync/actions/workflows/python.yml)
|
|
4
|
+
[](https://pypi.org/project/citesync/)
|
|
5
|
+
[](https://pypi.org/project/citesync/)
|
|
6
|
+
[](https://scorecard.dev/viewer/?uri=github.com/simonvanlierde/citesync)
|
|
7
|
+
[](https://github.com/simonvanlierde/citesync/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
Status: experimental. It may change in ways that break your code, without notice.
|
|
10
|
+
|
|
11
|
+
Questions and bugs: see [CONTRIBUTING.md](https://github.com/simonvanlierde/citesync/blob/main/.github/CONTRIBUTING.md).
|
|
12
|
+
|
|
13
|
+
Software citation metadata: citesync checks that CITATION.cff, codemeta.json, .zenodo.json and the package manifests (pyproject.toml, Cargo.toml, package.json) say the same thing. Where each field sits in each file comes from a mapping of CodeMeta's crosswalks, exported by [codemeta-drift](https://github.com/simonvanlierde/codemeta-drift).
|
|
14
|
+
|
|
15
|
+
## What works now
|
|
16
|
+
|
|
17
|
+
`citesync check` compares eight fields with one file, the source (CITATION.cff by default): title/name, version, authors with ORCID, licence, DOI, repository URL, description and keywords.
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
uv tool install citesync # or run it once with uvx citesync check
|
|
21
|
+
citesync check # exit 0: they agree; 1: an error finding; 2: citesync couldn't run
|
|
22
|
+
citesync check --source pyproject.toml
|
|
23
|
+
citesync check --root path/to/repo # check another directory
|
|
24
|
+
citesync check --format json # or --json; also --format github for workflow annotations
|
|
25
|
+
citesync check -v # also list fields that weren't compared (not-checked)
|
|
26
|
+
citesync check -q # only errors, no summary line
|
|
27
|
+
citesync rules # the rules, with their severity; --format json too
|
|
28
|
+
citesync explain version-mismatch # what a rule checks and how to fix it
|
|
29
|
+
citesync config # the config in use, and the file it came from
|
|
30
|
+
citesync write # rewrite the other files to agree with the source
|
|
31
|
+
citesync write --diff # print what write would change, and change nothing
|
|
32
|
+
citesync --version
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Run from a subdirectory, citesync finds the repo root as ruff does: it walks up to the nearest directory with a citation file or a citesync config, and stops at the git root, so a submodule keeps its own files. File paths in findings are relative to that root.
|
|
36
|
+
|
|
37
|
+
Each finding is one line, `file: severity[rule] field: message`, and a last line counts them:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
.zenodo.json:2:1: error[unreadable] Expecting property name enclosed in double quotes
|
|
41
|
+
codemeta.json: error[version-mismatch] version: '2.0.0', CITATION.cff has '1.0.0'
|
|
42
|
+
2 errors, 0 warnings in 3 files
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The severity is in colour on a terminal. `NO_COLOR` turns colour off, and `FORCE_COLOR` turns it on anywhere (`FORCE_COLOR=0` or `false` turns it off). `NO_COLOR` wins.
|
|
46
|
+
|
|
47
|
+
The rule ids name the field: `name-mismatch`, `version-mismatch`, `author-mismatch`, `license-mismatch`, `doi-mismatch`, `repository-mismatch`, `description-mismatch` and `keywords-mismatch` when a field differs from the source; `keywords-not-subset` for a manifest keyword the citation file doesn't have; `context-mismatch` when codemeta.json's `@context` isn't the configured CodeMeta version; `role-dangling` (warning) for a CodeMeta Role in codemeta.json whose person, named by ORCID, isn't listed; `unreadable` for a file citesync can't read; and `not-checked` (info) for a field that wasn't compared. [docs/rules.md](https://github.com/simonvanlierde/citesync/blob/main/docs/rules.md) explains each one. When the source has no place or no value for a field (pyproject.toml has no DOI), the other files that hold it must agree with each other.
|
|
48
|
+
|
|
49
|
+
A file citesync can't read or parse is an `unreadable` finding, with the parser's message and its line and column when the parser gives them. The other files are still checked against each other, and when the unreadable file is the source, each field it holds is reported as `not-checked`.
|
|
50
|
+
|
|
51
|
+
| Exit | When |
|
|
52
|
+
| ---: | --- |
|
|
53
|
+
| 0 | no error finding |
|
|
54
|
+
| 1 | an error finding, an unreadable file included |
|
|
55
|
+
| 2 | citesync couldn't run: a bad config or option, a missing source file, or an internal error. stdout is empty and stderr has one `citesync: <message>` line |
|
|
56
|
+
|
|
57
|
+
`--format json` prints one document:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"version": 1,
|
|
62
|
+
"tool": { "name": "citesync", "version": "0.1.0" },
|
|
63
|
+
"summary": { "files": 3, "errors": 1, "warnings": 0, "info": 0 },
|
|
64
|
+
"findings": [
|
|
65
|
+
{
|
|
66
|
+
"rule": "version-mismatch",
|
|
67
|
+
"severity": "error",
|
|
68
|
+
"file": "codemeta.json",
|
|
69
|
+
"term": "version",
|
|
70
|
+
"message": "'2.0.0', CITATION.cff has '1.0.0'",
|
|
71
|
+
"help": "Make the file agree with the source, or the other way round if the source is wrong.",
|
|
72
|
+
"url": "https://github.com/simonvanlierde/citesync/blob/main/docs/rules.md#version-mismatch",
|
|
73
|
+
"line": null,
|
|
74
|
+
"column": null
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Findings are sorted by file, rule and field, and positions count from 1. A value longer than 80 characters is cut to 77 and ends in `...` in the message. `file` is null for a field no file holds, and `term` (the CodeMeta term) is null for `unreadable`. JSON always lists every finding: `-q` and `-v` change only the text and GitHub output. [tests/output.schema.json](https://github.com/simonvanlierde/citesync/blob/main/tests/output.schema.json) is the JSON Schema. A minor release may add rule ids and optional fields; ignore what you don't know. Removing or retyping a field bumps `version`, in a major release. `--format github` writes one workflow annotation per finding, titled `citesync <rule>`, and ends with a notice that counts them all, since GitHub shows only the first ten of each level.
|
|
81
|
+
|
|
82
|
+
Spellings are normalised before comparing: SPDX ids, SPDX URLs and Zenodo's lowercase ids; ORCID URLs and bare iDs; Zenodo's `"Family, Given"` and npm or Cargo `"Name <email>"`; DOIs with or without `https://doi.org/`, and a codemeta `identifier` given as a schema.org PropertyValue (`propertyID` `DOI`, or a DOI as its `value` or `url`; other PropertyValues, such as a SWHID, are skipped); repository URLs as npm's hosted-git-info reads them: `git+`, `.git`, `http://`, `www.`, the host's case, npm shorthands (`github:owner/repo`, `owner/repo`), SSH remotes, and a branch, tag or file page on GitHub, GitLab, Codeberg, Gitea or Bitbucket (`…/tree/v1.0`, as Zenodo's GitHub integration writes), with GitHub paths in any case; CFF `name-particle` and `name-suffix`, read as part of the family name; a licence list, read as `A OR B`; PEP 621 url labels as packaging normalises them (`Source Code`, `homepage`); versions as PEP 440 versions (`v1.2.3` = `1.2.3`, `1.0` = `1.0.0`, `0.3.1-rc.1` = `0.3.1rc1`, `1.0.0-dev.1` = `1.0.0.dev1`), while a version that isn't one (`unreleased`) must match exactly. Names and keywords compare case-insensitively, with whitespace collapsed and Unicode in NFC; set `keywords-case-sensitive = true` to compare keywords by case too. Findings show each file's own spelling. Manifests are compared on version, authors' names, licence, repository URL and keywords, not on name or description. npm's one `author` is compared with the first author.
|
|
83
|
+
|
|
84
|
+
By default it reads every citation file present and the release manifests at the repo root: a pyproject.toml without `[build-system]` and a private package.json are skipped, a field in pyproject's `dynamic` is skipped on its own, and a Cargo workspace is read through `[workspace.package]`. The tests run each of the six files as the source against a fixture where all six agree, and CITATION.cff as the source against five real repositories.
|
|
85
|
+
|
|
86
|
+
`citesync write` fixes what `check` reports, and nothing more. `citesync write --diff` prints the same edits as a unified diff and writes nothing; it exits 1 when there is a change, or a file it can't read (listed on stderr), so CI can run it. It writes every citation file that isn't the source, and a manifest only for the fields listed in `write-manifests` (or all of them with `"all"`). A field a file leaves out stays out: a .zenodo.json without a version never gains one. When the source has no value for a field (no DOI in pyproject.toml), write leaves that field alone in the other files. It keeps each file's layout: comments, key order and quoting in CITATION.cff, comments in TOML, and in JSON the key order, indentation, one-line arrays and final newline. Each value is spelled the way that file spells it (Zenodo's `"Family, Given"` and lowercase licence ids, a DOI URL in codemeta.json, where a schema.org PropertyValue keeps its other keys, semver in Cargo.toml and package.json, PEP 440 in pyproject.toml). A value the file can't spell is left as it was, and its finding says why: a compound licence in .zenodo.json, a licence with `AND` or `WITH` in CITATION.cff or codemeta.json (`OR` becomes a list), in Cargo.toml and package.json a version semver has no spelling for (a post-release such as `1.2.3.post1`, an epoch, more than three numbers), and in pyproject.toml a version that isn't PEP 440. An author with only a name, such as an organization (`name` in CITATION.cff and codemeta.json), counts as a family name and keeps it. An author whose name or ORCID matches keeps the rest of their entry, such as an email or affiliation; anyone else gets a new entry. When that would drop an old entry holding more than a name and ORCID, such as after a renaming in a manifest, write leaves that file's authors for you to edit. A Cargo field inherited with `.workspace = true` is left as it is: change it in the workspace's `[workspace.package]`.
|
|
87
|
+
|
|
88
|
+
Values that compare equal stay as written: write leaves `v1.2.3` against `1.2.3`, and `LCA` against `lca` unless `keywords-case-sensitive` is set. A finding whose rule `ignore` lists is not written, and an `unreadable` file is never written. It prints `wrote <file>` for each file it changed (to stderr with `--format json` or `github`, and not at all with `--quiet`), then reports the findings left as `check` does, in the same formats, each with what to do: edit it by hand (with the reason, when write could have written it but didn't), or add the field to `write-manifests`. Exit 0 when no error finding remains, 1 when one does (an unreadable file included), and 2 when citesync couldn't run. Every file is written to a temporary file beside it before any is moved into place, so a failure leaves the files as they were, unless a move itself fails, which leaves the files moved before it written. A symlinked file is written through the link, CRLF line ends stay CRLF, and a file that would not change is not touched.
|
|
89
|
+
|
|
90
|
+
Known limits of the CITATION.cff round trip: an end-of-line comment aligned with spaces after a value (`title: x # c`) is reflowed, a second sequence indented differently from the first is reindented to match it, and a closing `...` and a byte-order mark are dropped.
|
|
91
|
+
|
|
92
|
+
Config goes in the first of `citesync.toml`, `.citesync.toml`, `[tool.citesync]` in pyproject.toml, `[workspace.metadata.citesync]` or `[package.metadata.citesync]` in Cargo.toml, or `"citesync"` in package.json:
|
|
93
|
+
|
|
94
|
+
```toml
|
|
95
|
+
source = "CITATION.cff" # any of the six files
|
|
96
|
+
codemeta-version = "3.0"
|
|
97
|
+
files = ["codemeta.json", ".zenodo.json"] # default: the citation files present
|
|
98
|
+
manifests = ["src/core/package.json"] # replaces the default root manifests
|
|
99
|
+
write-manifests = ["keywords", "license"] # or "all"; the manifest fields write may change
|
|
100
|
+
keywords-case-sensitive = true # default false: keywords compare as names do
|
|
101
|
+
ignore = ["description-mismatch"] # rule ids whose findings to drop
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`write-manifests` names fields by their CodeMeta term, as the JSON `term` does: `name`, `version`, `author`, `license`, `identifier` (the DOI), `codeRepository`, `description` and `keywords`. Rule ids use the plainer `doi` and `repository`.
|
|
105
|
+
|
|
106
|
+
An `ignore` entry that matched no finding prints a warning to stderr. An unknown rule id is a config error, and `unreadable` can't be ignored.
|
|
107
|
+
|
|
108
|
+
Not built yet: creating a citation file listed under `files` that doesn't exist yet.
|
|
109
|
+
|
|
110
|
+
## Pre-commit hook
|
|
111
|
+
|
|
112
|
+
For [prek](https://prek.j178.dev) or [pre-commit](https://pre-commit.com), in `.pre-commit-config.yaml`:
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
repos:
|
|
116
|
+
- repo: https://github.com/simonvanlierde/citesync
|
|
117
|
+
rev: v0.1.0
|
|
118
|
+
hooks:
|
|
119
|
+
- id: citesync
|
|
120
|
+
- id: citesync-write
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The hook runs `citesync check` from the repo root when a commit changes CITATION.cff, codemeta.json, .zenodo.json, pyproject.toml, Cargo.toml, package.json, `citesync.toml` or `.citesync.toml`, at any depth. It always checks every file, not only the staged ones, and fails the commit on an error finding. `citesync-write` runs `citesync write` only when you ask for it: `prek run citesync-write --stage manual --all-files`.
|
|
124
|
+
|
|
125
|
+
## Tested on real repositories
|
|
126
|
+
|
|
127
|
+
The tests copy the metadata files of five public repositories byte for byte (commits in [tests/fixtures/SOURCES.txt](https://github.com/simonvanlierde/citesync/blob/main/tests/fixtures/SOURCES.txt)) and check that citesync reports exactly these findings, with CITATION.cff as the source.
|
|
128
|
+
|
|
129
|
+
| Repository | Config | Findings | Exit | Why |
|
|
130
|
+
| --- | --- | --- | ---: | --- |
|
|
131
|
+
| sourceledger | none | codemeta.json `context-mismatch` | 1 | codemeta.json uses the CodeMeta 4.0 context, and citesync expects 3.0 unless `codemeta-version` says otherwise. pyproject.toml and .zenodo.json agree on every field |
|
|
132
|
+
| credit-matrix | none | codemeta.json `name-mismatch` | 1 | codemeta.json names it "CRediT Matrix", CITATION.cff "CRediT Matrix: drafting CRediT contribution statements with co-authors". The root package.json is private, so it is skipped |
|
|
133
|
+
| credit-matrix | `manifests = ["src/core/package.json"]` | codemeta.json `name-mismatch` | 1 | As above. The package agrees: its version, licence and author match, its keywords are a subset, and its `git+…/credit-matrix.git` repository names the same repository |
|
|
134
|
+
| jsonvet | none | Cargo.toml `keywords-not-subset` | 1 | The workspace keywords include `lint`, and CITATION.cff has `linter`. Only Cargo.toml holds a version, so there is nothing to compare it with |
|
|
135
|
+
| jsonvet | `manifests = ["crates/jsonvet-cli/pyproject.toml"]` | that pyproject.toml `keywords-not-subset`; `not-checked` version (info) | 1 | The same keyword. The list replaces the root manifests and the pyproject version is `dynamic`, so no file holds a version |
|
|
136
|
+
| r9lab | none | codemeta.json `context-mismatch` | 1 | As sourceledger. The root pyproject.toml has no `[build-system]` and the root package.json is private, so no manifest is checked |
|
|
137
|
+
| project-templates | none | `not-checked` version (info) | 0 | CITATION.cff alone, with no version |
|
|
138
|
+
|
|
139
|
+
A made-up set where all six files agree exits 0 with each of them as the source.
|
|
140
|
+
|
|
141
|
+
## Development
|
|
142
|
+
|
|
143
|
+
You need [uv](https://docs.astral.sh/uv/) and [just](https://just.systems).
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
uv sync
|
|
147
|
+
uv tool install prek # once per machine
|
|
148
|
+
prek install # git hooks
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`just` lists the tasks. They mirror what [.github/workflows/](https://github.com/simonvanlierde/citesync/tree/main/.github/workflows) runs.
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
just check # lint, types and tests for every stack here
|
|
155
|
+
just fmt # apply each stack's fixes and formatting
|
|
156
|
+
just hooks # the git hooks, as the checks workflow runs them
|
|
157
|
+
just mutation # mutation-test the comparison code (not in CI)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`just mutation` changes `check.py` and `codecs.py` one small step at a time and fails if the tests
|
|
161
|
+
still pass on a change that `tests/mutation-baseline.txt` does not list. A change that
|
|
162
|
+
cannot alter behaviour goes in that file with the reason. It also fails on a listed
|
|
163
|
+
change the tests now catch, so the list stays true.
|
|
164
|
+
|
|
165
|
+
uv locks only releases at least a week old. If `just check` fails on
|
|
166
|
+
a security fix newer than that, add the package to `exclude-newer-package` under `[tool.uv]` in `pyproject.toml`,
|
|
167
|
+
and remove it a week later ([how](https://github.com/simonvanlierde/project-templates/blob/main/docs/tooling.md#dependencies)).
|
|
168
|
+
|
|
169
|
+
## Releasing
|
|
170
|
+
|
|
171
|
+
To release, add a `## [X.Y.Z]` section to `CHANGELOG.md`, then push the tag `vX.Y.Z`.
|
|
172
|
+
`python-release.yml` publishes to PyPI, then creates the GitHub release with the distributions and their provenance bundle.
|
|
173
|
+
Approve the `pypi` environment when prompted. Do not create the GitHub release by hand.
|
|
174
|
+
If the release job fails after PyPI succeeds, delete any leftover draft with `gh release delete vX.Y.Z`.
|
|
175
|
+
Re-run only the `release` job. A published immutable release needs a new patch version to fix its files.
|
|
176
|
+
|
|
177
|
+
Before the first release, set up the publishing environment
|
|
178
|
+
(`pypi`)
|
|
179
|
+
in Settings > Environments: limit it to tags matching `v*`, and add a required reviewer.
|
|
180
|
+
The release workflows explain the rest in their header comments.
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
See [LICENSE](https://github.com/simonvanlierde/citesync/blob/main/LICENSE) for the license terms.
|