Carry set, shard and row licenses.
Makoto has no license field, and a verifier cannot establish that a source was licensed or legally usable (section 8.2). What it can do is carry a license claim, require the claim's shape, and let a reader refuse a step that arrives without one. Whether the claim is true stays with the people who review it.
Proposed extension. The vocabulary on this page, https://corpus.example/makoto/license/v1, is an example and not part of Makoto. A corpus would publish its own under a namespace it controls, or several corpora could agree on one shared profile. The mechanisms that carry it (extensions, profiles and policy rules) are current Makoto.
{
"authorizedKeyIds": [
"sha256:21981d07157626519dc4de7c1c09043877f8e66afd3554169bf9496df8419f50"
],
"id": "urn:makoto:example:rule:letters-ingest",
"minimumSignatures": 1,
"operationTypes": [
"urn:makoto:example:operation:corpus-ingest"
],
"predicateTypes": [
"https://usemakoto.dev/predicate/v0.2/transform"
],
"profileConstraints": [
{
"closureDigest": {
"sha256": "fbac8a68cb1a9ad0347e74c104ee885ed607bb7b837a4e1f3372163afc73c35d"
},
"digest": {
"sha256": "ce30589e17abff371e461b6fd75723b055fdddef6e865bcc5fabcf327f07b9ab"
},
"id": "https://corpus.example/makoto/license/v1/predicate.schema.json",
"target": "predicate"
}
]
}Three levels, three answers
| Level | Where the corpus keeps it | What Makoto does with it |
|---|---|---|
| Set | license and license_evidence in letters.yaml. | Proposed extension. The default in a claim on the ingest statement, validated by a profile. |
| Shard | license, attribution and license_evidence on one shard's entry in letters.yaml. | Proposed extension. An overrides entry keyed by the shard manifest's subjectName and the shard's entryName. |
| Row | license, attribution, source and maintainer in every row. | Nothing in this record. The shard's digest commits to every row, so a changed row changes the digest. These shards are dataset-manifest entries, and profiles apply to statement subjects, so no profile reads their rows. |
The claim
The ingest writes the claim into its statement's extensions (section 11.2). Extension keys are absolute HTTPS URIs or URNs, and the core schemas accept any such key. The claim sits on the ingest rather than the origin because per-shard overrides need shard names, which exist only once the ingest has run. This is the file passed to makoto attest transform --extensions:
{
"https://corpus.example/makoto/license/v1": {
"default": {
"evidence": "https://archive.example/harbour-letters/rights",
"license": "CC0-1.0"
},
"overrides": [
{
"attribution": "Transcribed by the Harbour Letters Society",
"entryName": "shards/letters-00001.ndjson",
"evidence": "https://archive.example/harbour-letters/society-terms",
"license": "CC-BY-4.0",
"subjectName": "letters.shards.json"
}
]
}
}
Shard letters-00001 is the only override; letters-00000 falls under the default. The manifest's own license fields and this claim are covered by the same signature, because the manifest's bytes are a subject of the same statement. Nothing checks that the two agree.
Require its shape with a profile
A profile is a JSON Schema pinned by digest (section 12.1). This one targets the predicate, so it validates the claim inside the signed statement:
{
"$schema": "https://usemakoto.dev/schema/v0.2/profile-dialect.schema.json",
"$id": "https://corpus.example/makoto/license/v1/predicate.schema.json",
"type": "object",
"required": [
"extensions"
],
"properties": {
"extensions": {
"type": "object",
"required": [
"https://corpus.example/makoto/license/v1"
],
"properties": {
"https://corpus.example/makoto/license/v1": {
"type": "object",
"required": [
"default",
"overrides"
],
"properties": {
"default": {
"type": "object",
"required": [
"license",
"evidence"
],
"properties": {
"license": {
"type": "string",
"minLength": 1
},
"attribution": {
"type": "string",
"minLength": 1
},
"evidence": {
"type": "string",
"minLength": 1
}
}
},
"overrides": {
"type": "array",
"items": {
"type": "object",
"required": [
"subjectName",
"entryName",
"license",
"evidence"
],
"properties": {
"subjectName": {
"type": "string",
"minLength": 1
},
"entryName": {
"type": "string",
"minLength": 1
},
"license": {
"type": "string",
"minLength": 1
},
"attribution": {
"type": "string",
"minLength": 1
},
"evidence": {
"type": "string",
"minLength": 1
}
}
}
}
}
}
}
}
}
}
makoto profile create turns the schema into a reference carrying its digest and closure digest. The ingest lists that reference in profiles, and the reference producer refuses to write the statement when the claim does not validate.
{"closureDigest":{"sha256":"fbac8a68cb1a9ad0347e74c104ee885ed607bb7b837a4e1f3372163afc73c35d"},"critical":true,"digest":{"sha256":"ce30589e17abff371e461b6fd75723b055fdddef6e865bcc5fabcf327f07b9ab"},"id":"https://corpus.example/makoto/license/v1/predicate.schema.json","resources":[],"target":"predicate"}
Refuse a step without it
The repository's policy authorizes corpus-ingest transforms only when the statement carries this exact profile, through profileConstraints (section 14):
{
"authorizedKeyIds": [
"sha256:21981d07157626519dc4de7c1c09043877f8e66afd3554169bf9496df8419f50"
],
"id": "urn:makoto:example:rule:letters-ingest",
"minimumSignatures": 1,
"operationTypes": [
"urn:makoto:example:operation:corpus-ingest"
],
"predicateTypes": [
"https://usemakoto.dev/predicate/v0.2/transform"
],
"profileConstraints": [
{
"closureDigest": {
"sha256": "fbac8a68cb1a9ad0347e74c104ee885ed607bb7b837a4e1f3372163afc73c35d"
},
"digest": {
"sha256": "ce30589e17abff371e461b6fd75723b055fdddef6e865bcc5fabcf327f07b9ab"
},
"id": "https://corpus.example/makoto/license/v1/predicate.schema.json",
"target": "predicate"
}
]
}
Here is the same ingest, signed by the same key, without the claim. The signature is valid, but no rule authorizes the statement, so the graph behind the handoff cannot be completed:
$ makoto verify bundle text/letters/makoto \
--policy lineage/policy.json \
--schema-catalog lineage/catalog.json \
--expected-artifact ../expected-letters.json
PASS load-safely
PASS parse-strictly
PASS index-payloads
PASS core-schemas
PASS signatures
FAIL authorization-thresholds
PASS metadata-profiles
FAIL authorization
PASS graph-dependency-artifacts
SKIPPED graph
SKIPPED roots-and-heads
FAIL completeness-anchor
PASS freshness-anchors
SKIPPED artifact-bytes
SKIPPED artifact-profiles
PASS decision
DENY decision
manifest sha256:d9deee4b8ca966a6b1207111f0d40fde78b8b2a7bb7c1fd2aeb8e2758572f8f5
E_SIGNER_UNAUTHORIZED: no matching policy rule met its signature threshold
E_MANIFEST_SET: final artifact is not an exact terminal subject of its declared head
E_MANIFEST_SET: historical artifact mapping is not an exact manifest-listed signed subject
E_MANIFEST_SET: signed manifest sets do not equal the verified graph and bundle index
E_SIGNER_UNAUTHORIZED is the cause. The E_MANIFEST_SET lines follow from it: the unauthorized statement drops out of the graph, which then no longer matches what the handoff names.
What this establishes
It establishes that each ingest statement in a record that verifies was signed by a key the reader trusts for ingests, carries a license claim of the shape the profile requires, and cannot change without its signature failing.
It does not establish that the claim is legally correct. The evidence URL points at the terms a reviewer should read, and the reader's own review decides. The profile checks shape only: it wants non-empty strings, not valid SPDX expressions, and nothing resolves the claim's subjectName and entryName against the record the way the verifier resolves inputs[].provenance.entryName. A stricter profile, or a reviewer, would have to.
Rows need a different shape. The verifier can validate every line of an NDJSON subject against an artifact profile (section 12.4). A corpus that named each JSON or NDJSON shard as its own subject, instead of listing it in a dataset manifest, could therefore require a license on every row. Parquet shards cannot be checked this way; the verifier hashes and sizes them and reads nothing inside.
Next: maintain the corpus through Git.
Sign-off, pull requests, and a check that refuses a manifest edited without its lineage record.