1use serde_json::Value;
10
11pub fn manifest_schema() -> Value {
13 serde_json::json!({
14 "$schema": "https://json-schema.org/draft/2020-12/schema",
15 "$id": "nap://schema/manifest.json",
16 "title": "NAP Manifest",
17 "description": concat!(
18 "The durable representation of a narrative resource. ",
19 "Human-editable, machine-readable, agent-queryable. ",
20 "Characters, locations, scenes, props, and worlds ",
21 "all share this common structure."
22 ),
23 "type": "object",
24 "required": ["id", "name", "entity_type"],
25 "properties": {
26 "id": {
27 "type": "string",
28 "pattern": "^nap://[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$",
29 "description": concat!(
30 "Canonical NAP URI. ",
31 "e.g., nap://toystory/character/woody"
32 )
33 },
34 "name": {
35 "type": "string",
36 "description": "Human-readable display name. e.g., 'Woody'"
37 },
38 "entity_type": {
39 "type": "string",
40 "minLength": 1,
41 "description": "The kind of narrative entity this manifest describes (any non-empty string)"
42 },
43 "version": {
44 "type": "integer",
45 "minimum": 0,
46 "description": "Monotonic version counter. Incremented on each commit."
47 },
48 "principals": {
49 "type": "object",
50 "description": "Access control owners/maintainers/publishers (optional).",
51 "properties": {
52 "owners": {
53 "type": "array",
54 "items": { "type": "string" },
55 "description": "Full control — can modify, transfer, delete."
56 },
57 "maintainers": {
58 "type": "array",
59 "items": { "type": "string" },
60 "description": "Can modify content but not transfer ownership."
61 },
62 "publishers": {
63 "type": "array",
64 "items": { "type": "string" },
65 "description": "Can publish/distribute but not modify source."
66 }
67 }
68 },
69 "properties": {
70 "type": "object",
71 "additionalProperties": true,
72 "description": concat!(
73 "Entity-specific key-value properties. ",
74 "Character: toy_type, homeworld, affiliation, accessory. ",
75 "Scene: setting, participants, mood, time_of_day, outcome. ",
76 "Location: climate, type, controlled_by, population. ",
77 "Prop: owner, material, weight, color."
78 )
79 },
80 "representations": {
81 "type": "object",
82 "additionalProperties": {
83 "$ref": "#/definitions/Representation"
84 },
85 "description": concat!(
86 "Content-addressed representations of this entity. ",
87 "Keys are labels like 'reference_image', 'voice_model', 'mesh'."
88 )
89 },
90 "references": {
91 "type": "object",
92 "additionalProperties": true,
93 "description": concat!(
94 "Cross-references to other NAP resources. ",
95 "Common keys: appears_in (array of scene URIs), ",
96 "relationships (array of {target, type} objects), ",
97 "owner (character URI)."
98 )
99 },
100 "provenance": {
101 "$ref": "#/definitions/Provenance",
102 "description": concat!(
103 "AI generation provenance metadata — which model, ",
104 "prompt, seed, and parameters were used."
105 )
106 },
107 "metadata": {
108 "type": "object",
109 "additionalProperties": true,
110 "description": "Arbitrary extension metadata. Future-proof escape hatch."
111 }
112 },
113 "definitions": {
114 "Representation": {
115 "type": "object",
116 "required": ["hash", "format"],
117 "properties": {
118 "hash": {
119 "type": "string",
120 "pattern": "^blake3:[a-f0-9]{64}$",
121 "description": concat!(
122 "BLAKE3 content hash of the asset. ",
123 "Format: blake3:<64-hex-chars>"
124 )
125 },
126 "format": {
127 "type": "string",
128 "description": concat!(
129 "File format. ",
130 "Examples: png, glb, onnx, wav, mp4, splat"
131 )
132 },
133 "uri": {
134 "type": "string",
135 "description": concat!(
136 "Optional storage URI. ",
137 "e.g., gs://assets/toystory/woody/ref.png, ",
138 "s3://bucket/path/to/file.glb"
139 )
140 },
141 "tier": {
142 "type": "string",
143 "enum": ["draft", "production", "distribution"],
144 "description": concat!(
145 "Quality tier of the representation. ",
146 "draft = WIP, production = final, distribution = optimized"
147 )
148 }
149 }
150 },
151 "Provenance": {
152 "type": "object",
153 "properties": {
154 "model": {
155 "type": "string",
156 "description": concat!(
157 "AI model used to generate this entity. ",
158 "e.g., 'midjourney-v6', 'gpt-4o', 'stable-diffusion-3'"
159 )
160 },
161 "prompt_hash": {
162 "type": "string",
163 "pattern": "^blake3:[a-f0-9]{64}$",
164 "description": "BLAKE3 content hash of the generation prompt."
165 },
166 "seed": {
167 "type": "string",
168 "description": "Generation seed for reproducibility."
169 },
170 "parameters": {
171 "type": "object",
172 "additionalProperties": {
173 "type": "string"
174 },
175 "description": concat!(
176 "Additional generation parameters. ",
177 "e.g., stylize, chaos, temperature, cfg_scale"
178 )
179 },
180 "derived_from": {
181 "type": "string",
182 "pattern": "^nap://",
183 "description": concat!(
184 "Parent entity URI this was derived from. ",
185 "e.g., nap://toystory/character/woody/v1"
186 )
187 },
188 "created_at": {
189 "type": "string",
190 "format": "date-time",
191 "description": "ISO 8601 creation timestamp."
192 }
193 }
194 }
195 }
196 })
197}
198
199pub fn commit_schema() -> Value {
201 serde_json::json!({
202 "$schema": "https://json-schema.org/draft/2020-12/schema",
203 "$id": "nap://schema/commit.json",
204 "title": "NAP Commit",
205 "description": concat!(
206 "A NAP commit records a point-in-time snapshot of a manifest ",
207 "plus patch metadata describing what changed."
208 ),
209 "type": "object",
210 "required": ["id", "timestamp", "author", "message", "manifest_hash"],
211 "properties": {
212 "id": {
213 "type": "string",
214 "pattern": "^[a-f0-9]{64}$",
215 "description": "BLAKE3 content-addressed commit identifier."
216 },
217 "parent": {
218 "type": "string",
219 "pattern": "^[a-f0-9]{64}$",
220 "description": "Parent commit hash. null for the initial commit."
221 },
222 "timestamp": {
223 "type": "string",
224 "format": "date-time",
225 "description": "ISO 8601 timestamp of when the commit was created."
226 },
227 "author": {
228 "type": "string",
229 "description": concat!(
230 "Author identifier ",
231 "(DID key, email, or key fingerprint)."
232 )
233 },
234 "signature": {
235 "type": "string",
236 "description": concat!(
237 "Ed25519 signature over the commit hash ",
238 "(optional in v0)."
239 )
240 },
241 "message": {
242 "type": "string",
243 "description": "Human-readable commit message describing the changes."
244 },
245 "manifest_hash": {
246 "type": "string",
247 "pattern": "^blake3:[a-f0-9]{64}$",
248 "description": concat!(
249 "BLAKE3 content hash of the resulting manifest ",
250 "after this commit."
251 )
252 },
253 "changes": {
254 "type": "array",
255 "items": {
256 "$ref": "#/definitions/Change"
257 },
258 "description": "Patch metadata describing what changed in this commit."
259 }
260 },
261 "definitions": {
262 "Change": {
263 "type": "object",
264 "required": ["path", "operation"],
265 "properties": {
266 "path": {
267 "type": "string",
268 "description": concat!(
269 "Dot-notation path to the changed field. ",
270 "e.g., 'properties.homeworld', ",
271 "'representations.reference_image.hash'"
272 )
273 },
274 "operation": {
275 "type": "string",
276 "enum": ["set", "delete", "append", "remove"],
277 "description": "The kind of change operation."
278 },
279 "old_value": {
280 "type": "string",
281 "description": "Previous value hash (for verification)."
282 },
283 "new_value": {
284 "type": "string",
285 "description": "New value hash or literal."
286 }
287 }
288 }
289 }
290 })
291}
292
293pub fn validate_manifest(manifest: &crate::manifest::Manifest) -> Result<(), Vec<String>> {
298 let schema = manifest_schema();
299 let instance = serde_json::to_value(manifest)
300 .map_err(|e| vec![format!("manifest serialization error: {e}")])?;
301 let validator = jsonschema::validator_for(&schema)
302 .map_err(|e| vec![format!("schema compilation error: {e}")])?;
303 let errors: Vec<String> = validator
304 .iter_errors(&instance)
305 .map(|e| format!("{}: {}", e.instance_path, e))
306 .collect();
307 if errors.is_empty() {
308 Ok(())
309 } else {
310 Err(errors)
311 }
312}
313
314pub fn validate_commit(commit: &crate::commit::Commit) -> Result<(), Vec<String>> {
319 let schema = commit_schema();
320 let instance = serde_json::to_value(commit)
321 .map_err(|e| vec![format!("commit serialization error: {e}")])?;
322 let validator = jsonschema::validator_for(&schema)
323 .map_err(|e| vec![format!("schema compilation error: {e}")])?;
324 let errors: Vec<String> = validator
325 .iter_errors(&instance)
326 .map(|e| format!("{}: {}", e.instance_path, e))
327 .collect();
328 if errors.is_empty() {
329 Ok(())
330 } else {
331 Err(errors)
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn test_validate_valid_manifest() {
341 use crate::manifest::Manifest;
342 let m = Manifest::new(
343 "toystory",
344 crate::types::EntityType::new("character"),
345 "woody",
346 "Woody",
347 );
348 assert!(validate_manifest(&m).is_ok());
349 }
350
351 #[test]
352 fn test_validate_manifest_with_custom_entity_type() {
353 use crate::manifest::Manifest;
354 let m = Manifest::new(
355 "lab",
356 crate::types::EntityType::new("scientific_paper"),
357 "fusion-2024",
358 "Cold Fusion Results",
359 );
360 assert!(validate_manifest(&m).is_ok());
361 }
362
363 #[test]
364 fn test_validate_manifest_rejects_empty_entity_type() {
365 let json = serde_json::json!({
367 "id": "nap://toystory/character/woody",
368 "name": "Woody",
369 "entity_type": "",
370 "version": 1,
371 });
372 let schema = manifest_schema();
374 let validator = jsonschema::validator_for(&schema).unwrap();
375 let errors: Vec<String> = validator
376 .iter_errors(&json)
377 .map(|e| format!("{}: {}", e.instance_path, e))
378 .collect();
379 assert!(
380 !errors.is_empty(),
381 "expected validation errors for empty entity_type"
382 );
383 }
384
385 #[test]
386 fn test_manifest_schema_is_valid_json() {
387 let schema = manifest_schema();
388 assert!(schema.is_object());
390 assert!(schema.get("title").is_some());
391 assert!(schema.get("properties").is_some());
392 assert!(schema.get("definitions").is_some());
393
394 let props = schema.get("properties").unwrap();
396 let et = props.get("entity_type").unwrap();
397 assert_eq!(et.get("type").unwrap().as_str(), Some("string"));
398 assert!(et.get("enum").is_none());
399 }
400
401 #[test]
402 fn test_commit_schema_is_valid_json() {
403 let schema = commit_schema();
404 assert!(schema.is_object());
405 assert!(schema.get("title").is_some());
406 assert!(schema.get("properties").is_some());
407 assert!(schema.get("definitions").is_some());
408 }
409
410 #[test]
411 fn test_schemas_serialize_to_valid_json() {
412 let m = manifest_schema();
413 let json = serde_json::to_string(&m).unwrap();
414 let parsed: Value = serde_json::from_str(&json).unwrap();
416 assert_eq!(
417 parsed.get("title").unwrap().as_str().unwrap(),
418 "NAP Manifest"
419 );
420
421 let c = commit_schema();
422 let json = serde_json::to_string(&c).unwrap();
423 let parsed: Value = serde_json::from_str(&json).unwrap();
424 assert_eq!(parsed.get("title").unwrap().as_str().unwrap(), "NAP Commit");
425 }
426}