@heroiclands/package-build 23.4.0 → 23.5.0
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/CHANGELOG.md +34 -0
- package/README.md +29 -25
- package/assets/images/provenance.yaml +1 -0
- package/bin/package-build.mjs +259 -28
- package/ci/prettier.mjs +73 -0
- package/config.mjs +10 -0
- package/container.mjs +232 -1
- package/content-config.mjs +5 -1
- package/docs/api.md +100 -52
- package/docs/authoring/assets.md +16 -1
- package/docs/commands.md +154 -7
- package/docs/configuration.md +39 -2
- package/docs/content-format.md +30 -6
- package/docs/guides/e2e.md +38 -0
- package/docs/guides/icons.md +5 -0
- package/docs/guides/release-and-deploy.md +36 -1
- package/docs/reference/format-details.md +218 -50
- package/docs/reference/note-types.md +71 -71
- package/engine/asset-index.mjs +63 -22
- package/engine/asset-references.mjs +355 -0
- package/engine/asset-shipping.mjs +363 -0
- package/engine/asset-types.mjs +70 -0
- package/engine/base-compiler.mjs +24 -1
- package/engine/cdn-publish.mjs +618 -0
- package/engine/index.mjs +6 -0
- package/engine/note-vocabulary.mjs +40 -2
- package/engine/pathnames.mjs +21 -10
- package/engine/rank-ladder.mjs +40 -16
- package/engine/scenes.mjs +1 -1
- package/engine/standing-terms.mjs +3 -1
- package/package.json +4 -3
- package/packagebuild-metadata.jsonl +13 -13
- package/release.mjs +69 -7
- package/sohl/actors.mjs +12 -4
- package/sohl/default-item-art.mjs +23 -19
- package/types/config.d.mts +7 -0
- package/types/container.d.mts +137 -1
- package/types/engine/asset-index.d.mts +3 -2
- package/types/engine/asset-references.d.mts +136 -0
- package/types/engine/asset-shipping.d.mts +240 -0
- package/types/engine/asset-types.d.mts +36 -0
- package/types/engine/base-compiler.d.mts +7 -1
- package/types/engine/cdn-publish.d.mts +311 -0
- package/types/engine/index.d.mts +2 -0
- package/types/engine/pathnames.d.mts +2 -1
- package/types/engine/rank-ladder.d.mts +25 -6
- package/types/release.d.mts +5 -3
- package/types/sohl/default-item-art.d.mts +2 -1
|
@@ -75,8 +75,8 @@ These fields are accepted by every note type. A field's value can still be irrel
|
|
|
75
75
|
| `data.height` | metres or feet and inches | Height in metres or feet and inches. |
|
|
76
76
|
| `data.weight` | kilograms or pounds | Body weight in kilograms or pounds. |
|
|
77
77
|
| `data.frame` | string | Relative frame — one of `scant`, `light`, `medium`, `heavy` or `massive`. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
78
|
-
| `data.appearance.eye_color` | string | Eye colour: one of `amber`, `black`, `blue`, `brown`, `dark_brown`, `grey`, `green`, `hazel` or `
|
|
79
|
-
| `data.appearance.hair_color` | string | Hair colour: one of `auburn`, `black`, `blonde`, `brown`, `chestnut`, `dark_blonde`, `dark_brown`, `grey`, `greying_black`, `greying_brown`, `red`, `silver`, `strawberry_blond` or `white`. See [a being's look](format-details.md#a-beings-look) for what each word means.
|
|
78
|
+
| `data.appearance.eye_color` | string | Eye colour: one of `amber`, `black`, `blue`, `brown`, `dark_brown`, `gold`, `grey`, `green`, `hazel`, `red`, `pink`, `silver`, `violet`, `white` or `yellow`. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
79
|
+
| `data.appearance.hair_color` | string | Hair colour: one of `auburn`, `black`, `blonde`, `brown`, `chestnut`, `dark_blonde`, `dark_brown`, `grey`, `greying_auburn`, `greying_black`, `greying_blonde`, `greying_brown`, `greying_chestnut`, `greying_dark_blonde`, `greying_dark_brown`, `greying_light_brown`, `light_brown`, `greying_red`, `greying_strawberry_blond`, `red`, `silver`, `strawberry_blond` or `white`. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
80
80
|
| `data.appearance.skin_color` | string | Skin colour: one of `pale`, `fair`, `light`, `medium`, `olive`, `tawny`, `golden`, `tanned`, `swarthy`, `brown`, `dark`, `dark_brown` or `ebony`. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
81
81
|
| `data.appearance.complexion` | a list of words, such as `[weathered]` or `[]` | The skin's condition, as a list of `ashen`, `blotchy`, `chapped`, `clear`, `freckled`, `jaundiced`, `leathery`, `oily`, `pasty`, `pimpled`, `pockmarked`, `rough`, `ruddy`, `sallow`, `scabrous`, `scarred`, `smooth`, `sun_kissed`, `sunburnt`, `translucent`, `vitiligo`, `wan`, `weathered` or `wrinkled`: `[weathered, freckled]`, `[clear]` for unremarked skin, or `[]` where no condition is established. Required once a being states an appearance; a single word outside a list is an error. See [a being's look](format-details.md#a-beings-look) for what each word means. |
|
|
82
82
|
| `data.appearance.extra_features` | list of strings | The visible physical marks no word covers, each a short plain description of one: `a burn scar on the left forearm`, `a missing finger`. An entry stating a hair, eye or skin colour or a skin condition, gear or clothing, a voice, behaviour or manner, an impression, handedness, sterility or an apparent age is an error, as is any punctuation but a hyphen or apostrophe inside a word. See [a being's look](format-details.md#a-beings-look). |
|
|
@@ -118,78 +118,78 @@ These fields are accepted by every note type. A field's value can still be irrel
|
|
|
118
118
|
|
|
119
119
|
**Subtypes:** `guild`, `order`, `polity`, `faithtradition`, `arcanetradition`, `spirittradition`, `lineage`, `venture`, `criminal`, `governmental`, `fellowship`.
|
|
120
120
|
|
|
121
|
-
| Field | Shape | Meaning
|
|
122
|
-
| ------------------------- | ---------------------------------------------- |
|
|
123
|
-
| `data.icon` | an Address | The document's profile art — an `icon` address, resolved into `img`.
|
|
124
|
-
| `data.templatePriority` | number | Template priority; unset means the note is not a template.
|
|
125
|
-
| `data.events` | list of event entries | What happened to or at this subject — each entry one dated, attributed event: a founding, a war, a fall, or an occasion that recurs.
|
|
126
|
-
| `data.demonym` | string | What one member is called — a Vylarian.
|
|
127
|
-
| `data.epithet` | string | The by-name it is known by — a god's, an order's, a company's.
|
|
128
|
-
| `data.symbol` | string | Its emblem in words: a feather atop a golden scale, a chisel carving a star.
|
|
129
|
-
| `data.governance.model` | string | How the affiliation is governed, where it is.
|
|
130
|
-
| `data.governance.summary` | string | A sentence on how the governance actually works.
|
|
131
|
-
| `data.governance.ranks` | list of `{ level, title, description, lore? }` | The ladder of
|
|
132
|
-
| `data.governance.offices` | a map of named offices | Named offices, each with a description and optional dated holders.
|
|
133
|
-
| `data.seat` | an Address | Where the affiliation's authority sits.
|
|
134
|
-
| `data.population` | number | How many people it counts.
|
|
135
|
-
| `data.economy` | list of Addresses | What its economic life runs on — currencies, banking bodies, goods.
|
|
136
|
-
| `data.lore` | list of Addresses | Lore concerning it — the peoples it draws on, the god a faith venerates, its law, its calendar.
|
|
137
|
-
| `data.parents` | list of Addresses | Affiliations it is subordinate to.
|
|
138
|
-
| `data.culture` | an Address | The people this affiliation belongs to, as a culture lore note.
|
|
139
|
-
| `data.relations` | a map keyed by Address | Standing with other affiliations — aligned, unaligned, rival, nemesis.
|
|
121
|
+
| Field | Shape | Meaning |
|
|
122
|
+
| ------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
123
|
+
| `data.icon` | an Address | The document's profile art — an `icon` address, resolved into `img`. |
|
|
124
|
+
| `data.templatePriority` | number | Template priority; unset means the note is not a template. |
|
|
125
|
+
| `data.events` | list of event entries | What happened to or at this subject — each entry one dated, attributed event: a founding, a war, a fall, or an occasion that recurs. |
|
|
126
|
+
| `data.demonym` | string | What one member is called — a Vylarian. |
|
|
127
|
+
| `data.epithet` | string | The by-name it is known by — a god's, an order's, a company's. |
|
|
128
|
+
| `data.symbol` | string | Its emblem in words: a feather atop a golden scale, a chisel carving a star. |
|
|
129
|
+
| `data.governance.model` | string | How the affiliation is governed, where it is. |
|
|
130
|
+
| `data.governance.summary` | string | A sentence on how the governance actually works. |
|
|
131
|
+
| `data.governance.ranks` | list of `{ level, title, description, lore? }` | The ladder of standings the body confers: level 0 for those it excludes and level 1 for its members, both required, and higher rungs only where the body has them. |
|
|
132
|
+
| `data.governance.offices` | a map of named offices | Named offices, each with a description and optional dated holders. |
|
|
133
|
+
| `data.seat` | an Address | Where the affiliation's authority sits. |
|
|
134
|
+
| `data.population` | number | How many people it counts. |
|
|
135
|
+
| `data.economy` | list of Addresses | What its economic life runs on — currencies, banking bodies, goods. |
|
|
136
|
+
| `data.lore` | list of Addresses | Lore concerning it — the peoples it draws on, the god a faith venerates, its law, its calendar. |
|
|
137
|
+
| `data.parents` | list of Addresses | Affiliations it is subordinate to. |
|
|
138
|
+
| `data.culture` | an Address | The people this affiliation belongs to, as a culture lore note. |
|
|
139
|
+
| `data.relations` | a map keyed by Address | Standing with other affiliations — aligned, unaligned, rival, nemesis. |
|
|
140
140
|
|
|
141
141
|
**Inner keys** — a key a field's value does not declare is an error at its own line and column.
|
|
142
142
|
|
|
143
|
-
| Key | Shape | Required | Meaning
|
|
144
|
-
| ---------------------------------------------------- | ------------------------------------------------------------------------------------- | -------- |
|
|
145
|
-
| `data.events[].id` | an address segment | no | The event's name within its note — lowercase letters and digits.
|
|
146
|
-
| `data.events[].kind` | one `eventKind` value | no | What sort of thing happened.
|
|
147
|
-
| `data.events[].depth` | one `depth` value | no | How far the event's history reaches.
|
|
148
|
-
| `data.events[].when` | a date, or `"0.<day>"` | yes | When it happened, or the anchor and first instance of an event that recurs.
|
|
149
|
-
| `data.events[].until` | a date | no | When a continuous event ended, or where a recurring series stopped.
|
|
150
|
-
| `data.events[].recurs` | `{ every }` or `{ on }` | no | How further occurrences are found; absent for an event that happened once.
|
|
151
|
-
| `data.events[].recurs.every` | a whole number of years, 1 or more | no | A period counted on the canonical axis from `when`.
|
|
152
|
-
| `data.events[].recurs.on` | a list of dates, strictly increasing, each later than `when` | no | Recorded occurrences beyond the first, in place of a period.
|
|
153
|
-
| `data.events[].summary` | a string | yes | What happened, stated plainly.
|
|
154
|
-
| `data.events[].standing` | one `standing` value | no | What the world's evidence supports.
|
|
155
|
-
| `data.events[].names` | `{ name, by, gloss? }[]` | no | The event's names in the world, each with who uses it.
|
|
156
|
-
| `data.events[].names[].name` | a string | yes | One name the event goes by in the world.
|
|
157
|
-
| `data.events[].names[].by` | an Address naming an `affiliation`, `lore`, `place`, `being` or `skill` note | yes | Who uses that name — a people, a polity, a faith, a place, a tongue.
|
|
158
|
-
| `data.events[].names[].gloss` | a string | no | What the name means, or how it is used.
|
|
159
|
-
| `data.events[].where` | `{ locus?, reach? }` | no | Where it happened, and where it was felt.
|
|
160
|
-
| `data.events[].where.locus` | a list of Addresses, each defaulting to `place` | no | Where the event physically happened; each resolves to a place.
|
|
161
|
-
| `data.events[].where.reach` | `{ place, how, knowledge, attributedTo? }[]` | no | Where the event was felt, and whether each place knows why.
|
|
162
|
-
| `data.events[].where.reach[].place` | an Address, defaulting to `place` | yes | A place where the event was felt.
|
|
163
|
-
| `data.events[].where.reach[].how` | a string | yes | One clause: a consequence someone in that place could notice.
|
|
164
|
-
| `data.events[].where.reach[].knowledge` | one `knowledge` value | yes | Whether that place connects what it felt to this event.
|
|
165
|
-
| `data.events[].where.reach[].attributedTo` | an Address, defaulting to `lore`, naming an event or a `lore` note | no | Only beside `knowledge: misattributed` — the cause that place names instead.
|
|
166
|
-
| `data.events[].who` | `{ ref, role }[]` | no | Who took part, and as what.
|
|
167
|
-
| `data.events[].who[].ref` | an Address naming a `being`, `affiliation` or `lore` note | yes | A participant — a being, a people, an affiliation.
|
|
168
|
-
| `data.events[].who[].role` | one `role` value | yes | What the participant was to the event.
|
|
169
|
-
| `data.events[].follows` | `{ event, how, note? }[]` | no | The earlier events this one follows from.
|
|
170
|
-
| `data.events[].follows[].event` | an Address, defaulting to `lore`, anchored to an `event` where its note holds several | yes | The earlier event; it resolves to exactly one event.
|
|
171
|
-
| `data.events[].follows[].how` | one `followsHow` value | yes | How this event follows from it.
|
|
172
|
-
| `data.events[].follows[].note` | a string | no | One clause saying what connects the two.
|
|
173
|
-
| `data.events[].accounts` | `{ by, says, agrees, withholds? }[]` | no | What each people, polity, faith or place says about it.
|
|
174
|
-
| `data.events[].accounts[].by` | an Address naming an `affiliation`, `lore`, `place` or `being` note | yes | Who holds the account.
|
|
175
|
-
| `data.events[].accounts[].says` | a string | yes | What they say happened, in their terms.
|
|
176
|
-
| `data.events[].accounts[].agrees` | one `agrees` value | yes | How far their account agrees with `summary`.
|
|
177
|
-
| `data.events[].accounts[].withholds` | a string | no | What they decline to say.
|
|
178
|
-
| `data.events[].unresolved` | a list of strings | no | What the world itself has not settled.
|
|
179
|
-
| `data.events[].sources` | a list of Addresses | no | The notes that state or support the event; each resolves.
|
|
180
|
-
| `data.events[].stated` | `{ calendar, text }` | no | The date as one tradition writes it in its own reckoning.
|
|
181
|
-
| `data.events[].stated.calendar` | an Address, or a lore shortcode | yes | The calendar that tradition reckons in — a `lore` note with `subType: calendar`.
|
|
182
|
-
| `data.events[].stated.text` | a string | yes | The date as that tradition writes it.
|
|
183
|
-
| `data.governance.ranks[].level` | a whole number — the rung's position on this body's own ladder | yes | The rung's position on the ladder; a member's `rank` names it.
|
|
184
|
-
| `data.governance.ranks[].title` | what the standing is called | yes | What the standing is called.
|
|
185
|
-
| `data.governance.ranks[].description` | what the standing is | yes | What the standing is.
|
|
186
|
-
| `data.governance.ranks[].lore` | a lore Address | no | Lore saying more about the standing than a description holds.
|
|
187
|
-
| `data.governance.offices.<name>.description` | a string | yes | What the office is.
|
|
188
|
-
| `data.governance.offices.<name>.holders` | a list of `{ being, start?, end?, contested? }` | yes | Who has held it, each with the dates of the term.
|
|
189
|
-
| `data.governance.offices.<name>.holders[].being` | a being Address | yes | Who held the office.
|
|
190
|
-
| `data.governance.offices.<name>.holders[].start` | a date | no | When the term began; unstated, it began before anything recorded.
|
|
191
|
-
| `data.governance.offices.<name>.holders[].end` | a date | no | When the term ended; unstated, the being holds the office now.
|
|
192
|
-
| `data.governance.offices.<name>.holders[].contested` | `true` or `false` | no | The term may overlap another holder's.
|
|
143
|
+
| Key | Shape | Required | Meaning |
|
|
144
|
+
| ---------------------------------------------------- | ------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------- |
|
|
145
|
+
| `data.events[].id` | an address segment | no | The event's name within its note — lowercase letters and digits. |
|
|
146
|
+
| `data.events[].kind` | one `eventKind` value | no | What sort of thing happened. |
|
|
147
|
+
| `data.events[].depth` | one `depth` value | no | How far the event's history reaches. |
|
|
148
|
+
| `data.events[].when` | a date, or `"0.<day>"` | yes | When it happened, or the anchor and first instance of an event that recurs. |
|
|
149
|
+
| `data.events[].until` | a date | no | When a continuous event ended, or where a recurring series stopped. |
|
|
150
|
+
| `data.events[].recurs` | `{ every }` or `{ on }` | no | How further occurrences are found; absent for an event that happened once. |
|
|
151
|
+
| `data.events[].recurs.every` | a whole number of years, 1 or more | no | A period counted on the canonical axis from `when`. |
|
|
152
|
+
| `data.events[].recurs.on` | a list of dates, strictly increasing, each later than `when` | no | Recorded occurrences beyond the first, in place of a period. |
|
|
153
|
+
| `data.events[].summary` | a string | yes | What happened, stated plainly. |
|
|
154
|
+
| `data.events[].standing` | one `standing` value | no | What the world's evidence supports. |
|
|
155
|
+
| `data.events[].names` | `{ name, by, gloss? }[]` | no | The event's names in the world, each with who uses it. |
|
|
156
|
+
| `data.events[].names[].name` | a string | yes | One name the event goes by in the world. |
|
|
157
|
+
| `data.events[].names[].by` | an Address naming an `affiliation`, `lore`, `place`, `being` or `skill` note | yes | Who uses that name — a people, a polity, a faith, a place, a tongue. |
|
|
158
|
+
| `data.events[].names[].gloss` | a string | no | What the name means, or how it is used. |
|
|
159
|
+
| `data.events[].where` | `{ locus?, reach? }` | no | Where it happened, and where it was felt. |
|
|
160
|
+
| `data.events[].where.locus` | a list of Addresses, each defaulting to `place` | no | Where the event physically happened; each resolves to a place. |
|
|
161
|
+
| `data.events[].where.reach` | `{ place, how, knowledge, attributedTo? }[]` | no | Where the event was felt, and whether each place knows why. |
|
|
162
|
+
| `data.events[].where.reach[].place` | an Address, defaulting to `place` | yes | A place where the event was felt. |
|
|
163
|
+
| `data.events[].where.reach[].how` | a string | yes | One clause: a consequence someone in that place could notice. |
|
|
164
|
+
| `data.events[].where.reach[].knowledge` | one `knowledge` value | yes | Whether that place connects what it felt to this event. |
|
|
165
|
+
| `data.events[].where.reach[].attributedTo` | an Address, defaulting to `lore`, naming an event or a `lore` note | no | Only beside `knowledge: misattributed` — the cause that place names instead. |
|
|
166
|
+
| `data.events[].who` | `{ ref, role }[]` | no | Who took part, and as what. |
|
|
167
|
+
| `data.events[].who[].ref` | an Address naming a `being`, `affiliation` or `lore` note | yes | A participant — a being, a people, an affiliation. |
|
|
168
|
+
| `data.events[].who[].role` | one `role` value | yes | What the participant was to the event. |
|
|
169
|
+
| `data.events[].follows` | `{ event, how, note? }[]` | no | The earlier events this one follows from. |
|
|
170
|
+
| `data.events[].follows[].event` | an Address, defaulting to `lore`, anchored to an `event` where its note holds several | yes | The earlier event; it resolves to exactly one event. |
|
|
171
|
+
| `data.events[].follows[].how` | one `followsHow` value | yes | How this event follows from it. |
|
|
172
|
+
| `data.events[].follows[].note` | a string | no | One clause saying what connects the two. |
|
|
173
|
+
| `data.events[].accounts` | `{ by, says, agrees, withholds? }[]` | no | What each people, polity, faith or place says about it. |
|
|
174
|
+
| `data.events[].accounts[].by` | an Address naming an `affiliation`, `lore`, `place` or `being` note | yes | Who holds the account. |
|
|
175
|
+
| `data.events[].accounts[].says` | a string | yes | What they say happened, in their terms. |
|
|
176
|
+
| `data.events[].accounts[].agrees` | one `agrees` value | yes | How far their account agrees with `summary`. |
|
|
177
|
+
| `data.events[].accounts[].withholds` | a string | no | What they decline to say. |
|
|
178
|
+
| `data.events[].unresolved` | a list of strings | no | What the world itself has not settled. |
|
|
179
|
+
| `data.events[].sources` | a list of Addresses | no | The notes that state or support the event; each resolves. |
|
|
180
|
+
| `data.events[].stated` | `{ calendar, text }` | no | The date as one tradition writes it in its own reckoning. |
|
|
181
|
+
| `data.events[].stated.calendar` | an Address, or a lore shortcode | yes | The calendar that tradition reckons in — a `lore` note with `subType: calendar`. |
|
|
182
|
+
| `data.events[].stated.text` | a string | yes | The date as that tradition writes it. |
|
|
183
|
+
| `data.governance.ranks[].level` | a whole number — the rung's position on this body's own ladder | yes | The rung's position on the ladder — `0` the excluded, `1` the ordinary member; a member's `rank` names it. |
|
|
184
|
+
| `data.governance.ranks[].title` | what the standing is called | yes | What the standing is called. |
|
|
185
|
+
| `data.governance.ranks[].description` | what the standing is | yes | What the standing is. |
|
|
186
|
+
| `data.governance.ranks[].lore` | a lore Address | no | Lore saying more about the standing than a description holds. |
|
|
187
|
+
| `data.governance.offices.<name>.description` | a string | yes | What the office is. |
|
|
188
|
+
| `data.governance.offices.<name>.holders` | a list of `{ being, start?, end?, contested? }` | yes | Who has held it, each with the dates of the term. |
|
|
189
|
+
| `data.governance.offices.<name>.holders[].being` | a being Address | yes | Who held the office. |
|
|
190
|
+
| `data.governance.offices.<name>.holders[].start` | a date | no | When the term began; unstated, it began before anything recorded. |
|
|
191
|
+
| `data.governance.offices.<name>.holders[].end` | a date | no | When the term ended; unstated, the being holds the office now. |
|
|
192
|
+
| `data.governance.offices.<name>.holders[].contested` | `true` or `false` | no | The term may overlap another holder's. |
|
|
193
193
|
|
|
194
194
|
**System mappings**
|
|
195
195
|
|
package/engine/asset-index.mjs
CHANGED
|
@@ -27,12 +27,13 @@
|
|
|
27
27
|
* slug. A reader tells the two shapes apart by the `asset` block, which is what
|
|
28
28
|
* {@link module:engine/index-records.isAssetRecord} asks.
|
|
29
29
|
*
|
|
30
|
-
* **`path` is
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* it. Foundry
|
|
34
|
-
*
|
|
35
|
-
*
|
|
30
|
+
* **`path` is the source path.** The record is emitted by the package holding
|
|
31
|
+
* the bytes, so its path is that package's path — relative to that package's
|
|
32
|
+
* `assets/` directory — and names where the file sits in the repository. The
|
|
33
|
+
* book stages from it. Foundry and the website serve the **deployed** layout
|
|
34
|
+
* instead, `<icons|images>/<basename>`, which follows from the same path by
|
|
35
|
+
* dropping the directories between the type root and the file — see
|
|
36
|
+
* {@link module:engine/asset-shipping}.
|
|
36
37
|
*
|
|
37
38
|
* **Provenance resolves per address**, and the rule is stated rather than
|
|
38
39
|
* inferred from a directory:
|
|
@@ -43,8 +44,14 @@
|
|
|
43
44
|
* sidecar exists precisely because that answer is wrong for this one file.
|
|
44
45
|
* 2. Otherwise the nearest `provenance.yaml`, searching the file's own directory
|
|
45
46
|
* and then its ancestors, stopping at the type root.
|
|
46
|
-
* 3. Otherwise nothing
|
|
47
|
-
*
|
|
47
|
+
* 3. Otherwise nothing — and a file no record covers is a **finding** at the
|
|
48
|
+
* file. Every asset states who holds its rights, the terms it is used under
|
|
49
|
+
* and whether it is machine-generated, because the stage decides from those
|
|
50
|
+
* three answers whether the file ships.
|
|
51
|
+
*
|
|
52
|
+
* Resolution is **per file**, never per folder. One directory routinely holds
|
|
53
|
+
* files covered by its own `provenance.yaml` beside files a sidecar answers for,
|
|
54
|
+
* so nothing reads one file's answer as its neighbour's.
|
|
48
55
|
*
|
|
49
56
|
* An unknown key in a provenance file is a **finding** rather than a silent
|
|
50
57
|
* drop: `licence` beside `license` is otherwise an attribution record that looks
|
|
@@ -66,7 +73,7 @@ import path from "node:path";
|
|
|
66
73
|
import YAML from "yaml";
|
|
67
74
|
|
|
68
75
|
import { canonicalKey } from "./content-address.mjs";
|
|
69
|
-
import { positionOfYamlPath } from "./diagnostics.mjs";
|
|
76
|
+
import { formatDiagnostic, positionOfYamlPath } from "./diagnostics.mjs";
|
|
70
77
|
import { ASSET_SYSTEM, ASSET_TYPES, imageDimensions, isAssetShortcode } from "./asset-types.mjs";
|
|
71
78
|
|
|
72
79
|
/**
|
|
@@ -164,10 +171,12 @@ export const ASSET_RECORD_FIELDS = Object.freeze([
|
|
|
164
171
|
Object.freeze({
|
|
165
172
|
name: "ai",
|
|
166
173
|
from: "provenance",
|
|
174
|
+
required: true,
|
|
167
175
|
type: "boolean",
|
|
168
176
|
describe:
|
|
169
177
|
"Whether the file is machine-generated — the YAML boolean `true` " +
|
|
170
|
-
"or `false`, never a string."
|
|
178
|
+
"or `false`, never a string. Required: a record that does not say " +
|
|
179
|
+
"is a finding, never read as `false`.",
|
|
171
180
|
}),
|
|
172
181
|
Object.freeze({
|
|
173
182
|
name: "license",
|
|
@@ -257,6 +266,17 @@ const PROVENANCE_FIELD_BY_KEY = new Map(
|
|
|
257
266
|
]),
|
|
258
267
|
);
|
|
259
268
|
|
|
269
|
+
/**
|
|
270
|
+
* What a file carries when its record omits a required key, by key.
|
|
271
|
+
*
|
|
272
|
+
* @type {Readonly<Record<string, string>>}
|
|
273
|
+
*/
|
|
274
|
+
const MISSING_KEY_CONSEQUENCE = Object.freeze({
|
|
275
|
+
attribution: "no attribution",
|
|
276
|
+
license: "no licence",
|
|
277
|
+
ai: "no statement of whether it is machine-generated — and an unstated `ai` is never read as `false`, so none of them ships",
|
|
278
|
+
});
|
|
279
|
+
|
|
260
280
|
/**
|
|
261
281
|
* Read one provenance file, reporting every key that is not a provenance key.
|
|
262
282
|
*
|
|
@@ -289,7 +309,9 @@ function readProvenanceFile(file, findings) {
|
|
|
289
309
|
});
|
|
290
310
|
return {};
|
|
291
311
|
}
|
|
292
|
-
|
|
312
|
+
// An empty file is a record that states nothing, and is held to the
|
|
313
|
+
// required keys below like any other.
|
|
314
|
+
if (parsed == null) parsed = {};
|
|
293
315
|
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
294
316
|
findings.push({
|
|
295
317
|
file,
|
|
@@ -357,21 +379,24 @@ function readProvenanceFile(file, findings) {
|
|
|
357
379
|
}
|
|
358
380
|
|
|
359
381
|
// A record resolves wholesale — the nearest one is the whole answer, and no
|
|
360
|
-
// ancestor fills a key it leaves out. So an omitted `attribution`
|
|
361
|
-
// `
|
|
362
|
-
// rights holder
|
|
363
|
-
//
|
|
364
|
-
//
|
|
382
|
+
// ancestor fills a key it leaves out. So an omitted `attribution`, `license`
|
|
383
|
+
// or `ai` is not a value inherited from above; it is a file with no stated
|
|
384
|
+
// rights holder, no stated terms, or no statement of whether a machine made
|
|
385
|
+
// it, which is the one thing a provenance record exists to prevent.
|
|
386
|
+
// Reported against the file rather than a line, because the fault is an
|
|
387
|
+
// absence and has no position. An `ai` that is present but not a boolean
|
|
388
|
+
// already has its own finding above.
|
|
365
389
|
for (const key of REQUIRED_PROVENANCE_KEYS) {
|
|
366
|
-
|
|
390
|
+
const field = PROVENANCE_FIELD_BY_KEY.get(key);
|
|
391
|
+
const stated = field?.type === "boolean" ? Object.hasOwn(parsed, key) : Boolean(out[key]);
|
|
392
|
+
if (stated) continue;
|
|
367
393
|
findings.push({
|
|
368
394
|
file,
|
|
369
395
|
severity: "error",
|
|
370
396
|
message:
|
|
371
397
|
`a provenance record states \`${key}\`, and this one does not. ` +
|
|
372
398
|
"A record replaces rather than extends the one above it, so every " +
|
|
373
|
-
|
|
374
|
-
(key === "license" ? "licence" : key),
|
|
399
|
+
`file it covers would carry ${MISSING_KEY_CONSEQUENCE[key] ?? `no ${key}`}`,
|
|
375
400
|
});
|
|
376
401
|
}
|
|
377
402
|
return out;
|
|
@@ -497,8 +522,9 @@ function* walkAssetRoot(rootDir, extensions) {
|
|
|
497
522
|
* asserts that the address does not exist.
|
|
498
523
|
* @returns {Array<Record<string, any>>} One record per addressable file, in walk
|
|
499
524
|
* order; the caller imposes the index's total order.
|
|
500
|
-
* @throws {Error} When
|
|
501
|
-
* supplied.
|
|
525
|
+
* @throws {Error} When any finding is made and no `problems` array was
|
|
526
|
+
* supplied. Its message is every finding as a located diagnostic line, one
|
|
527
|
+
* per line; `file` and `position` name the first.
|
|
502
528
|
*/
|
|
503
529
|
export function collectAssetRecords(assetsBase, { contentPackage, problems }) {
|
|
504
530
|
const records = [];
|
|
@@ -548,6 +574,17 @@ export function collectAssetRecords(assetsBase, { contentPackage, problems }) {
|
|
|
548
574
|
// ancestor walk is not consulted when one is present.
|
|
549
575
|
: inheritedProvenance(path.dirname(absPath), rootDir, cache, findings);
|
|
550
576
|
|
|
577
|
+
if (provenance === null) {
|
|
578
|
+
report(
|
|
579
|
+
absPath,
|
|
580
|
+
`no provenance record covers this file — add \`${path.basename(absPath)}` +
|
|
581
|
+
`${PROVENANCE_SIDECAR_SUFFIX}\` beside it, or a \`${PROVENANCE_FILE}\` ` +
|
|
582
|
+
`in its directory or one above it up to ${root}/. Every asset states ` +
|
|
583
|
+
"its attribution, its licence and whether it is machine-generated, " +
|
|
584
|
+
"and the stage ships a file only on those answers",
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
551
588
|
for (const field of ASSET_RECORD_FIELDS) {
|
|
552
589
|
if (field.onlyType && field.onlyType !== type && provenance?.[field.name]) {
|
|
553
590
|
report(
|
|
@@ -584,10 +621,14 @@ export function collectAssetRecords(assetsBase, { contentPackage, problems }) {
|
|
|
584
621
|
|
|
585
622
|
if (findings.length) {
|
|
586
623
|
if (!problems) {
|
|
624
|
+
// Every finding, each a located diagnostic line, so a tree with a
|
|
625
|
+
// dozen uncovered files is fixed in one pass rather than one
|
|
626
|
+
// rebuild per file.
|
|
587
627
|
const first = findings[0];
|
|
588
|
-
const err = new Error(
|
|
628
|
+
const err = new Error(findings.map(formatDiagnostic).join("\n"));
|
|
589
629
|
err.file = first.file;
|
|
590
630
|
if (first.line) err.position = { line: first.line, column: first.column };
|
|
631
|
+
/** @type {any} */ (err).located = true;
|
|
591
632
|
throw err;
|
|
592
633
|
}
|
|
593
634
|
problems.push(...findings);
|