@cloudhal/core-contracts 0.3.3 → 0.3.5

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.
@@ -1,425 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://cloudhal.dev/schemas/v1/comment.schema.json",
4
- "title": "Comment",
5
- "description": "A reader comment on a published content entry, as stored in D1 (outside the site graph), returned to people and agents, and exported (RFC-0007). It is not a graph node: it never enters a snapshot, a diff or a site bundle, and a content rollback never changes it. The record carries no personal data beyond the display name the commenter chose: an email address, an IP address or its hash, a user agent and a proof-of-work challenge live in separate columns and tables that this contract cannot express (additionalProperties: false), so no reader of the contract — Studio lists, MCP tools, exports — can leak them. Text is plain text; renderers escape it and turn only https links into anchors with rel=\"ugc nofollow noopener noreferrer\".",
6
- "type": "object",
7
- "additionalProperties": false,
8
- "required": [
9
- "schemaVersion",
10
- "id",
11
- "siteId",
12
- "nodeId",
13
- "source",
14
- "author",
15
- "status",
16
- "createdAt",
17
- "rev"
18
- ],
19
- "properties": {
20
- "schemaVersion": {
21
- "$ref": "#/$defs/SchemaVersion"
22
- },
23
- "id": {
24
- "$ref": "#/$defs/CommentId"
25
- },
26
- "siteId": {
27
- "$ref": "#/$defs/Id"
28
- },
29
- "nodeId": {
30
- "$ref": "#/$defs/Id",
31
- "description": "The content entry the comment belongs to, by id, so threads survive slug changes (RFC-0002)."
32
- },
33
- "parentId": {
34
- "$ref": "#/$defs/CommentId",
35
- "description": "The comment this one is nested under for display. Absent for a top-level comment. In v1 the tree is two levels deep: a parent is always top-level, and a reply to a reply is nested under the same top-level comment with `inReplyToId` pointing at the comment it answers."
36
- },
37
- "inReplyToId": {
38
- "$ref": "#/$defs/CommentId",
39
- "description": "The comment this one answers when that is not `parentId` itself (a reply to a reply). Only with `parentId`, in the same thread."
40
- },
41
- "source": {
42
- "type": "string",
43
- "description": "Where the comment came from: `native` (the site's own form or Studio), `import` (an importer; see `origin`). `webmention` and `activitypub` are reserved for later RFCs and are refused by v1 writers.",
44
- "enum": [
45
- "native",
46
- "import",
47
- "webmention",
48
- "activitypub"
49
- ]
50
- },
51
- "origin": {
52
- "$ref": "#/$defs/CommentOrigin"
53
- },
54
- "author": {
55
- "$ref": "#/$defs/CommentAuthor"
56
- },
57
- "body": {
58
- "$ref": "#/$defs/CommentBody"
59
- },
60
- "status": {
61
- "$ref": "#/$defs/CommentStatus"
62
- },
63
- "labels": {
64
- "type": "array",
65
- "description": "Advisory labels from rules, classifiers, agents and people. Labels never change what readers see; only a status change does.",
66
- "items": {
67
- "$ref": "#/$defs/CommentLabel"
68
- },
69
- "maxItems": 32
70
- },
71
- "moderation": {
72
- "$ref": "#/$defs/CommentModeration"
73
- },
74
- "createdAt": {
75
- "$ref": "#/$defs/Timestamp"
76
- },
77
- "editedAt": {
78
- "$ref": "#/$defs/Timestamp",
79
- "description": "Set when the text changed after it was first stored (v1.1 edit window; staff replies edited in Studio)."
80
- },
81
- "rev": {
82
- "$ref": "#/$defs/Revision"
83
- }
84
- },
85
- "$defs": {
86
- "Actor": {
87
- "type": "object",
88
- "additionalProperties": false,
89
- "required": [
90
- "kind",
91
- "id"
92
- ],
93
- "properties": {
94
- "kind": {
95
- "$ref": "#/$defs/ActorKind"
96
- },
97
- "id": {
98
- "$ref": "#/$defs/Id"
99
- },
100
- "name": {
101
- "type": "string",
102
- "maxLength": 200
103
- }
104
- }
105
- },
106
- "ActorKind": {
107
- "type": "string",
108
- "description": "Who performed an action. Humans and agents share one actor model so the policy engine treats them uniformly.",
109
- "enum": [
110
- "human",
111
- "agent",
112
- "system"
113
- ]
114
- },
115
- "CommentAuthor": {
116
- "description": "Who wrote the comment, discriminated by `kind`. There is deliberately no email, address or account handle here.",
117
- "oneOf": [
118
- {
119
- "type": "object",
120
- "title": "GuestCommentAuthor",
121
- "description": "A guest: only the display name they typed. Guest comments are always held for moderation in v1.",
122
- "additionalProperties": false,
123
- "required": [
124
- "kind",
125
- "name"
126
- ],
127
- "properties": {
128
- "kind": {
129
- "type": "string",
130
- "enum": [
131
- "guest"
132
- ]
133
- },
134
- "name": {
135
- "$ref": "#/$defs/CommentDisplayName"
136
- }
137
- }
138
- },
139
- {
140
- "type": "object",
141
- "title": "ReaderCommentAuthor",
142
- "description": "A verified reader (v1.1: email code or magic link on the site's own host). `readerId` is the site's reader account id, never the email.",
143
- "additionalProperties": false,
144
- "required": [
145
- "kind",
146
- "readerId",
147
- "name"
148
- ],
149
- "properties": {
150
- "kind": {
151
- "type": "string",
152
- "enum": [
153
- "reader"
154
- ]
155
- },
156
- "readerId": {
157
- "$ref": "#/$defs/Id"
158
- },
159
- "name": {
160
- "$ref": "#/$defs/CommentDisplayName"
161
- }
162
- }
163
- },
164
- {
165
- "type": "object",
166
- "title": "StaffCommentAuthor",
167
- "description": "A reply on the site's behalf. It is always attributed to a person (`personId` is a staff actor id, `user_…`): an agent may draft the text (`draftedBy`), but a person approved it. `draftedBy` names the agent that drafted the text, when one did (shown in Studio and the audit, not on the public page).",
168
- "additionalProperties": false,
169
- "required": [
170
- "kind",
171
- "personId",
172
- "name",
173
- "badge"
174
- ],
175
- "properties": {
176
- "kind": {
177
- "type": "string",
178
- "enum": [
179
- "staff"
180
- ]
181
- },
182
- "personId": {
183
- "type": "string",
184
- "description": "The person who wrote or approved the reply.",
185
- "pattern": "^user_[A-Za-z0-9]{6,32}$"
186
- },
187
- "name": {
188
- "$ref": "#/$defs/CommentDisplayName"
189
- },
190
- "badge": {
191
- "type": "string",
192
- "description": "What the public page shows next to the name: `author` when the person wrote the entry, otherwise `team`.",
193
- "enum": [
194
- "author",
195
- "team"
196
- ]
197
- },
198
- "draftedBy": {
199
- "$ref": "#/$defs/Actor"
200
- }
201
- }
202
- },
203
- {
204
- "type": "object",
205
- "title": "ErasedCommentAuthor",
206
- "description": "The commenter's identity was erased on request (RFC-0007 privacy model). Pages show a localised \"Anonymous\".",
207
- "additionalProperties": false,
208
- "required": [
209
- "kind"
210
- ],
211
- "properties": {
212
- "kind": {
213
- "type": "string",
214
- "enum": [
215
- "erased"
216
- ]
217
- }
218
- }
219
- }
220
- ]
221
- },
222
- "CommentBody": {
223
- "type": "object",
224
- "description": "The comment text. v1 has only `text`: plain text with line breaks, no markup. A later RFC may add a restricted Markdown format. Absent only when the text was deleted (status `deleted`, or an erasure that removed the text); the record then stays as a tombstone while approved replies hang under it.",
225
- "additionalProperties": false,
226
- "required": [
227
- "format",
228
- "text"
229
- ],
230
- "properties": {
231
- "format": {
232
- "type": "string",
233
- "enum": [
234
- "text"
235
- ]
236
- },
237
- "text": {
238
- "type": "string",
239
- "description": "NFC-normalised, CRLF turned into LF, trimmed: 1 to 5,000 characters. Tabs and line feeds are the only control characters allowed; bidirectional overrides are refused so a comment cannot disguise itself in Studio.",
240
- "minLength": 1,
241
- "maxLength": 5000,
242
- "pattern": "^[^\\x00-\\x08\\x0B-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]+$"
243
- }
244
- }
245
- },
246
- "CommentDisplayName": {
247
- "type": "string",
248
- "description": "Plain text chosen by the commenter (or the staff member's name), NFC-normalised and trimmed by the server: 1 to 80 characters, no control characters, no bidirectional overrides, no surrounding whitespace.",
249
- "minLength": 1,
250
- "maxLength": 80,
251
- "pattern": "^[^\\s\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069](?:[^\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]*[^\\s\\x00-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069])?$"
252
- },
253
- "CommentId": {
254
- "type": "string",
255
- "description": "A comment id: `cmt_` and 6 to 32 letters or digits.",
256
- "pattern": "^cmt_[A-Za-z0-9]{6,32}$"
257
- },
258
- "CommentLabel": {
259
- "type": "object",
260
- "description": "An advisory label. `rule` labels come from the submission pipeline (word lists, link count, instruction-like text); `classifier` from an opt-in classifier (Workers AI, Akismet); `agent` from `label_comments`; `person` from Studio. `by` is the agent or person that set it (absent for rules and classifiers).",
261
- "additionalProperties": false,
262
- "required": [
263
- "label",
264
- "source",
265
- "at"
266
- ],
267
- "properties": {
268
- "label": {
269
- "type": "string",
270
- "enum": [
271
- "spam-suspect",
272
- "toxic",
273
- "off-topic",
274
- "question",
275
- "needs-reply",
276
- "praise",
277
- "personal-data",
278
- "links",
279
- "watch-word",
280
- "instruction-like"
281
- ]
282
- },
283
- "source": {
284
- "type": "string",
285
- "enum": [
286
- "rule",
287
- "classifier",
288
- "agent",
289
- "person"
290
- ]
291
- },
292
- "by": {
293
- "$ref": "#/$defs/Actor"
294
- },
295
- "confidence": {
296
- "type": "integer",
297
- "description": "0 to 100, when the source reports one.",
298
- "minimum": 0,
299
- "maximum": 100
300
- },
301
- "at": {
302
- "$ref": "#/$defs/Timestamp"
303
- }
304
- }
305
- },
306
- "CommentModeration": {
307
- "type": "object",
308
- "description": "The latest moderation decision, kept as a statement of reasons (EU DSA Art. 17). The full history is in `comment_events`. Approvals are always a person's decision; agents may only hide, mark as spam or hold.",
309
- "additionalProperties": false,
310
- "required": [
311
- "action",
312
- "actor",
313
- "at",
314
- "automated"
315
- ],
316
- "properties": {
317
- "action": {
318
- "type": "string",
319
- "enum": [
320
- "approve",
321
- "hide",
322
- "spam",
323
- "hold",
324
- "restore",
325
- "delete",
326
- "erase"
327
- ]
328
- },
329
- "actor": {
330
- "$ref": "#/$defs/Actor"
331
- },
332
- "at": {
333
- "$ref": "#/$defs/Timestamp"
334
- },
335
- "reasonCode": {
336
- "type": "string",
337
- "description": "Required by writers for hide, spam, delete and erase.",
338
- "enum": [
339
- "spam",
340
- "abuse",
341
- "off-topic",
342
- "personal-data",
343
- "illegal-content",
344
- "legal-order",
345
- "author-request",
346
- "erasure-request",
347
- "duplicate",
348
- "other"
349
- ]
350
- },
351
- "note": {
352
- "type": "string",
353
- "description": "Free text from the moderator, plain text, at most 500 characters.",
354
- "maxLength": 500,
355
- "pattern": "^[^\\x00-\\x08\\x0B-\\x1F\\x7F\\u202A-\\u202E\\u2066-\\u2069]*$"
356
- },
357
- "automated": {
358
- "type": "boolean",
359
- "description": "True when the decision was taken without a person (an agent, a rule or a ban), as a statement of reasons must say."
360
- }
361
- }
362
- },
363
- "CommentOrigin": {
364
- "type": "object",
365
- "description": "For imported comments: the system and its id, so importers are idempotent.",
366
- "additionalProperties": false,
367
- "required": [
368
- "system",
369
- "id"
370
- ],
371
- "properties": {
372
- "system": {
373
- "type": "string",
374
- "enum": [
375
- "cloudhal",
376
- "disqus",
377
- "wordpress"
378
- ]
379
- },
380
- "id": {
381
- "type": "string",
382
- "description": "The comment's id in that system: printable ASCII, at most 200 characters.",
383
- "minLength": 1,
384
- "maxLength": 200,
385
- "pattern": "^[!-~]+$"
386
- }
387
- }
388
- },
389
- "CommentStatus": {
390
- "type": "string",
391
- "description": "Lifecycle. `pending` waits for a person (every guest comment starts here); `approved` is public; `hidden` is withdrawn and reversible; `spam` is never shown and is deleted after the retention period; `deleted` is a soft delete, hard-deleted after the retention period unless approved replies keep it as a tombstone.",
392
- "enum": [
393
- "pending",
394
- "approved",
395
- "hidden",
396
- "spam",
397
- "deleted"
398
- ]
399
- },
400
- "Id": {
401
- "type": "string",
402
- "description": "Prefixed, URL-safe identifier, e.g. site_01HZX8K3Q9. The prefix names the entity kind; agents and the MCP server pass these as opaque handles.",
403
- "pattern": "^[a-z]+_[A-Za-z0-9]{6,32}$",
404
- "minLength": 8,
405
- "maxLength": 48
406
- },
407
- "Revision": {
408
- "type": "integer",
409
- "description": "Monotonic per-node revision used for optimistic locking (compare-and-swap on write).",
410
- "minimum": 0
411
- },
412
- "SchemaVersion": {
413
- "type": "integer",
414
- "description": "Version of this contract. Readers reject unknown versions; migrations move documents forward one version at a time.",
415
- "enum": [
416
- 1
417
- ]
418
- },
419
- "Timestamp": {
420
- "type": "string",
421
- "description": "RFC 3339 / ISO 8601 instant in UTC.",
422
- "format": "date-time"
423
- }
424
- }
425
- }