Repository: washingtonpost/ans-schema
Pull request #158 (2018-01-09 02:35:01)

Multisite 0.6.0
Adds proposal and basic implementation of multi-site schema


Patch:
diff --git a/docs/proposals/2018-01-08_-_MultiSite.md b/docs/proposals/2018-01-08_-_MultiSite.md
new file mode 100644
index 0000000..1481bfe
--- /dev/null
+++ b/docs/proposals/2018-01-08_-_MultiSite.md
@@ -0,0 +1,123 @@
+# Multi-Site Support in ANS
+
+To support publishing to multiple `website` destinations in Arc, new fields are being added. Existing single-site organizations can ignore these fields without losing functionality, although they are encourage to begin using the new fields when appropriate.
+
+## Changes
+
+### New Reference Type -- Section
+
+The new `section` type supplants the previous `site` type. References to a `section` must also specify a `website` in addition to an `id` for denormalization purposes.  `site` objects and references will continue to be supported, but will not denormalize against any website except the "default" website for an organization.
+
+### New Fields
+
+* `taxonomy.sections` - A replacement for `taxonomy.sites` but with the new section type.  A list of all sections across all websites that this document belongs to.
+
+* `taxonomy.primary_section` - A partial replacement for `taxonomy.primary_site`. The primary section (within a website) that this document belongs to.
+
+* `canonical_website` - Specified on an ANS website to indicate which website, of all websites within an organization, should be considered the canonical host of this document for SEO purposes. Typically this is the website that originally created the document.
+
+* `website` - Available only when fetching from Content API for this release. Indicates which `website` copy is being rendered.
+
+* `website_url` - Available only when fetching from Content API for this release. Indicates what url to use for url lookups when the specified `website` is requested.
+
+* `websites` - A global object populated with website-specific data. Each key in this object represents a website for which this content exists (e.g., is published to.). Each key points to an object which may contain up to two fields:
+  * `website_section` - The website-specific section (or section reference) that this document belongs to. Unlike taxonomy.sections and taxonomy.primary_section, this is not queryable. May be used by the URL Service to generate URLs.
+  * `website_url` - The website-specific url for the keyed website. May be denormalized from URL service if not present.
+
+### New Behavior
+
+`canonical_url` -- In 0.6.0 documents, `canonical_url`, if not provided at ingestion time, will calculated by looking for the `website_url` value for the `canonical_website` in the `websites` dictionary. If `websites` and `canonical_website` are not present, will denormalize from URL service as before.
+
+## Example
+
+```json
+{
+  "type": "story",
+  "version": "0.6.0",
+
+  "canonical_website": "latimes",
+  "canonical_url": "/sports/lakers/yay-lakers-win",
+
+  "website": "tribune",
+  "website_url": "/sports/bulls/boo-bulls-lose",
+
+  "websites": {
+    "latimes": {
+      "website_section": {
+        "type": "section",
+        "version": "0.6.0",
+        "_id": "/sports/lakers",
+        "_website": "latimes",
+        "name": "Lakers"
+      }
+    },
+
+    "tribune": {
+      "website_section": {
+        "type": "reference",
+        "referent": {
+          "type": "section",
+          "id": "/sports/bulls",
+          "website": "tribune"
+        }
+      }
+    },
+
+    "balsun": {
+      "website_url": "/sports/lakers-defeat-bulls"
+    }
+  },
+
+
+  "headlines": {
+    "basic": "Lakers Defeat Bulls"
+  },
+
+
+  "content_elements": [
+    {
+      "type": "text",
+      "content": "The Lakers defeated the Bulls!"
+    }
+  ],
+
+
+  "taxonomy": {
+    "primary_section": {
+      "type": "reference",
+      "referent": {
+        "type": "section",
+        "id": "/sports",
+        "website": "latimes"
+      }
+    },
+    "sections": [
+      {
+        "type": "section",
+        "version": "0.6.0",
+        "_id": "/sports/bulls",
+        "_website": "tribune",
+        "name": "Bulls",
+        "parent_id": "/sports",
+        "parent": {
+          "default": "/sports"
+        }
+      },
+      {
+        "type": "section",
+        "version": "0.6.0",
+        "_id": "/sports/lakers",
+        "_website": "latimes",
+        "name": "Lakers"
+      },
+      {
+        "type": "section",
+        "version": "0.6.0",
+        "_id": "/sports",
+        "_website": "balsun",
+        "name": "Sports"
+      }
+    ]
+  }
+}
+```
diff --git a/lib/transforms.js b/lib/transforms.js
index acf6855..a9506c2 100644
--- a/lib/transforms.js
+++ b/lib/transforms.js
@@ -46,14 +46,6 @@ var doUpvert = function doUpvert(ans, version_to) {
                     " because it is not less than " + version_to.str());
   }
 
-  // Don't upvert across minor or major version changes for now
-  if (version_from.major !== version_to.major ||
-      version_from.minor !== version_to.minor) {
-    throw new Error("Upverting from " + version_from.str() + " to " +
-                    version_to.version + " is not supported at this time");
-  }
-
-
   if (_.has(upverters, version_from.str())) {
     var transformer = upverters[version_from.str()]
     return doUpvert(transformer(ans), version_to);
diff --git a/lib/validator.js b/lib/validator.js
index 6a747ed..e13fc91 100644
--- a/lib/validator.js
+++ b/lib/validator.js
@@ -15,7 +15,7 @@ var TYPES = [];
 
 var VERSIONS = _.map(
   _.filter(version.history, function (item) {
-    return item.name.indexOf("0.5") > -1;
+    return (item.name.indexOf("0.5") > -1) || (item.name.indexOf("0.6") > -1);
   }),
   function(item) {
     return item.name;
diff --git a/package.json b/package.json
index a69c8c7..d92bf40 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
     "node-options": "^0.0.7",
     "ncp": "2.0.0",
     "rimraf": "2.5.2",
-    "async": "2.0.0-rc.6",
+    "async": "2.0.0",
     "replacestream": "4.0.0",
     "should": "~8.2.1",
     "mocha": "2.4.5"
@@ -25,7 +25,7 @@
   },
   "main": "./lib/ans.js",
   "scripts": {
-    "test": "mocha tests/schema-tests-04.js tests/schema-tests-05.js tests/transform-tests tests/validator",
+    "test": "mocha tests/schema-tests-04.js tests/schema-tests-05.js tests/schema-tests-06.js tests/transform-tests tests/validator",
     "ans": "node lib/cli.js"
   },
   "bin": {
diff --git a/src/main/resources/schema/ans/0.6.0/gallery.json b/src/main/resources/schema/ans/0.6.0/gallery.json
index 33449fd..e1f20c4 100644
--- a/src/main/resources/schema/ans/0.6.0/gallery.json
+++ b/src/main/resources/schema/ans/0.6.0/gallery.json
@@ -36,6 +36,15 @@
     "canonical_url": {
       "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_canonical_url.json"
     },
+    "canonical_website": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_canonical_website.json"
+    },
+    "website": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_website.json"
+    },
+    "website_url": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_website_url.json"
+    },
     "short_url": {
       "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_short_url.json"
     },
@@ -133,7 +142,12 @@
 
     "content_elements": {
       "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_content_elements.json"
+    },
+
+    "websites": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_websites.json"
     }
+
   },
   "required": [ "type", "version" ]
 }
diff --git a/src/main/resources/schema/ans/0.6.0/story.json b/src/main/resources/schema/ans/0.6.0/story.json
index 514881a..59f6067 100644
--- a/src/main/resources/schema/ans/0.6.0/story.json
+++ b/src/main/resources/schema/ans/0.6.0/story.json
@@ -35,6 +35,15 @@
     "canonical_url": {
       "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_canonical_url.json"
     },
+    "canonical_website": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_canonical_website.json"
+    },
+    "website": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_website.json"
+    },
+    "website_url": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_website_url.json"
+    },
     "short_url": {
       "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_short_url.json"
     },
@@ -141,6 +150,10 @@
     },
     "voice_transcripts": {
       "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_voice_transcripts.json"
+    },
+
+    "websites": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_websites.json"
     }
 
   },
diff --git a/src/main/resources/schema/ans/0.6.0/traits/trait_canonical_url.json b/src/main/resources/schema/ans/0.6.0/traits/trait_canonical_url.json
index 551bd2b..0fa443f 100644
--- a/src/main/resources/schema/ans/0.6.0/traits/trait_canonical_url.json
+++ b/src/main/resources/schema/ans/0.6.0/traits/trait_canonical_url.json
@@ -2,6 +2,6 @@
   "$schema": "http://json-schema.org/draft-04/schema#",
   "id": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_canonical_url.json",
   "title": "Canonical URL",
-  "description": "The fully qualified URL to the story. In the Arc ecosystem, this will be populated by the content api from the arc-canonical-url service if present.",
+  "description": "The relative URL to this document on the website specified by the `canonical_website` field. In the Arc ecosystem, this will be populated by the content api from the arc-canonical-url service if present based on the canonical_website. In conjunction with canonical_website, it can be used to determine the SEO canonical url or open graph url. In a multi-site context, this field may be different from the website_url field.",
   "type": "string"
 }
diff --git a/src/main/resources/schema/ans/0.6.0/traits/trait_canonical_website.json b/src/main/resources/schema/ans/0.6.0/traits/trait_canonical_website.json
new file mode 100644
index 0000000..a4745f3
--- /dev/null
+++ b/src/main/resources/schema/ans/0.6.0/traits/trait_canonical_website.json
@@ -0,0 +1,7 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "id": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_canonical_website.json",
+  "title": "Canonical Website",
+  "description": "The _id of the website from which this document was originally authored. In conjunction with canonical_url, it can be used to determine the SEO canonical url or open graph url. In a multi-site context, this field may be different from the website field.",
+  "type": "string"
+}
diff --git a/src/main/resources/schema/ans/0.6.0/traits/trait_taxonomy.json b/src/main/resources/schema/ans/0.6.0/traits/trait_taxonomy.json
index ca83844..0a61257 100644
--- a/src/main/resources/schema/ans/0.6.0/traits/trait_taxonomy.json
+++ b/src/main/resources/schema/ans/0.6.0/traits/trait_taxonomy.json
@@ -40,8 +40,9 @@
         "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/utils/tag.json"
       }
     },
+
     "primary_site": {
-      "description": "A primary site object or reference to one. In the Arc ecosystem, a reference here is denormalized into a site from the arc-site-service.",
+      "description": "Deprecated in 0.6.0. (See `primary_section` instead.) A primary site object or reference to one. In the Arc ecosystem, a reference here is denormalized into a site from the arc-site-service.",
       "oneOf": [
         {
           "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/utils/site.json"
@@ -66,8 +67,36 @@
         }
       ]
     },
+
+    "primary_section": {
+      "description": "A primary section object or reference to one. In the Arc ecosystem, a reference here is denormalized into a site from the arc-site-service.",
+      "oneOf": [
+        {
+          "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/utils/section.json"
+        },
+        {
+          "allOf": [
+            {
+              "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/utils/reference.json"
+            },
+            {
+              "properties": {
+                "referent": {
+                  "properties": {
+                    "type": {
+                      "enum": [ "section" ]
+                    }
+                  }
+                }
+              }
+            }
+          ]
+        }
+      ]
+    },
+
     "sites": {
-      "description": "A list of site objects or references to them. In the Arc ecosystem, references in this list are denormalized into sites from the arc-site-service.",
+      "description": "Deprecated in 0.6.0. (See `sections` instead.) A list of site objects or references to them. In the Arc ecosystem, references in this list are denormalized into sites from the arc-site-service.  In a multi-site context, sites will be denormalized against an organization's default website only.",
       "type": "array",
       "items": {
         "oneOf": [
@@ -95,6 +124,37 @@
         ]
       }
     },
+
+    "sections": {
+      "description": "A list of site objects or references to them. In the Arc ecosystem, references in this list are denormalized into sites from the arc-site-service.  In a multi-site context, sites will be denormalized against an organization's default website only.",
+      "type": "array",
+      "items": {
+        "oneOf": [
+          {
+            "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/utils/section.json"
+          },
+          {
+            "allOf": [
+              {
+                "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/utils/reference.json"
+              },
+              {
+                "properties": {
+                  "referent": {
+                    "properties": {
+                      "type": {
+                        "enum": [ "section" ]
+                      }
+                    }
+                  }
+                }
+              }
+            ]
+          }
+        ]
+      }
+    },
+
     "seo_keywords": {
       "type": "array",
       "description": "A list of user-editable manually entered keywords for search purposes. In the Arc ecosystem, these can be generated and saved in source CMS systems, editors, etc.",
diff --git a/src/main/resources/schema/ans/0.6.0/traits/trait_website.json b/src/main/resources/schema/ans/0.6.0/traits/trait_website.json
new file mode 100644
index 0000000..c7c2aa5
--- /dev/null
+++ b/src/main/resources/schema/ans/0.6.0/traits/trait_website.json
@@ -0,0 +1,7 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "id": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_website.json",
+  "title": "Website",
+  "description": "The _id of the website on which this document exists. This field is only available in Content API. If different from canonical_website, then this document was originally sourced from the canonical_website. Generated at fetch time by Content API.",
+  "type": "string"
+}
diff --git a/src/main/resources/schema/ans/0.6.0/traits/trait_website_url.json b/src/main/resources/schema/ans/0.6.0/traits/trait_website_url.json
new file mode 100644
index 0000000..33810e3
--- /dev/null
+++ b/src/main/resources/schema/ans/0.6.0/traits/trait_website_url.json
@@ -0,0 +1,7 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "id": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_website_url.json",
+  "title": "Website URL",
+  "description": "The relative URL to this document on the website specified by the `website` field. In a multi-site context, this is the url that is typically queried on when fetching by URL. It may be different than canonical_url. Generated at fetch time by Content API.",
+  "type": "string"
+}
diff --git a/src/main/resources/schema/ans/0.6.0/traits/trait_websites.json b/src/main/resources/schema/ans/0.6.0/traits/trait_websites.json
new file mode 100644
index 0000000..43343e5
--- /dev/null
+++ b/src/main/resources/schema/ans/0.6.0/traits/trait_websites.json
@@ -0,0 +1,28 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "id": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_websites.json",
+  "title": "Websites Input",
+  "description": "Website-specific  metadata for url generation for multi-site copies. These fields are not indexed in Content API.",
+  "type": "object",
+  "patternProperties": {
+    "^[a-zA-Z0-9_]*": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "website_section": {
+          "anyOf": [
+            {
+              "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/utils/reference.json"
+            },
+            {
+              "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/utils/section.json"
+            }
+          ]
+        },
+        "website_url": {
+          "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_website_url.json"
+        }
+      }
+    }
+  }
+}
diff --git a/src/main/resources/schema/ans/0.6.0/utils/reference.json b/src/main/resources/schema/ans/0.6.0/utils/reference.json
index d134898..0cddcfb 100644
--- a/src/main/resources/schema/ans/0.6.0/utils/reference.json
+++ b/src/main/resources/schema/ans/0.6.0/utils/reference.json
@@ -46,13 +46,27 @@
           "description": "A URL that can resolve the id into an ANS element",
           "type": "string"
         },
+        "website": {
+          "description": "The website which the referenced id belongs to. Currently supported only for sections.",
+          "type": "string"
+        },
         "referent_properties": {
           "additionalProperties": {},
           "description": "An object for key-value pairs that should override the values of keys with the same name in the denormalized object",
           "type": "object"
         }
       },
-      "required": [ "provider", "id" ]
+      "dependencies": {
+        "website": {
+          "properties": {
+            "type": {
+              "type": "string",
+              "enum": [ "section" ]
+            }
+          }
+        }
+      },
+      "required": [ "id" ]
     }
   },
   "required": [ "type", "referent" ]
diff --git a/src/main/resources/schema/ans/0.6.0/utils/section.json b/src/main/resources/schema/ans/0.6.0/utils/section.json
new file mode 100644
index 0000000..2328bcf
--- /dev/null
+++ b/src/main/resources/schema/ans/0.6.0/utils/section.json
@@ -0,0 +1,56 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "id": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/utils/section.json",
+  "title": "Section",
+  "description": "A hierarchical section in a taxonomy. In the Arc ecosystem, these are stored in the arc-site-service.",
+  "type": "object",
+  "additionalProperties": false,
+  "properties": {
+    "type": {
+      "enum": [ "section" ]
+    },
+    "_id" : {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_id.json"
+    },
+    "_website": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_website.json"
+    },
+    "version": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_version.json"
+    },
+    "additional_properties": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_additional_properties.json"
+    },
+    "name": {
+      "description": "The name of this site",
+      "type": "string"
+    },
+    "description": {
+      "description": "A short description or tagline about this site",
+      "type": "string"
+    },
+    "path": {
+      "description": "The url path to this site",
+      "type": "string"
+    },
+    "parent_id": {
+      "description": "The id of this section's parent section in the default hierarchy, if any.",
+      "type": "string"
+    },
+    "parent": {
+      "description": "The id of this section's parent section in various commonly-used hierarchies, where available.",
+      "type": "object",
+      "properties": {
+        "default": {
+          "type": "string"
+        }
+      }
+    },
+
+    "primary": {
+      "description": "Is this the primary site?",
+      "type": "boolean"
+    }
+  },
+  "required": [ "type", "version", "name" ]
+}
diff --git a/src/main/resources/schema/ans/0.6.0/video.json b/src/main/resources/schema/ans/0.6.0/video.json
index d7f4034..7dcfc49 100644
--- a/src/main/resources/schema/ans/0.6.0/video.json
+++ b/src/main/resources/schema/ans/0.6.0/video.json
@@ -35,6 +35,15 @@
     "canonical_url": {
       "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_canonical_url.json"
     },
+    "canonical_website": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_canonical_website.json"
+    },
+    "website": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_website.json"
+    },
+    "website_url": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_website_url.json"
+     },
     "short_url": {
       "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_short_url.json"
     },
@@ -171,7 +180,11 @@
     },
     "corrections": {
       "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_corrections.json"
+    },
+    "websites": {
+      "$ref": "https://raw.githubusercontent.com/washingtonpost/ans-schema/master/src/main/resources/schema/ans/0.6.0/traits/trait_websites.json"
     }
+
   },
   "required": [ "type", "version" ]
 }


Test patch:
diff --git a/tests/fixtures/schema/0.6.0/story-fixture-good-multi-site.json b/tests/fixtures/schema/0.6.0/story-fixture-good-multi-site.json
new file mode 100644
index 0000000..39988da
--- /dev/null
+++ b/tests/fixtures/schema/0.6.0/story-fixture-good-multi-site.json
@@ -0,0 +1,89 @@
+{
+  "type": "story",
+  "version": "0.6.0",
+
+  "canonical_website": "latimes",
+  "canonical_url": "/sports/lakers/yay-lakers-win",
+
+  "website": "tribune",
+  "website_url": "/sports/bulls/boo-bulls-lose",
+
+  "websites": {
+    "latimes": {
+      "website_section": {
+        "type": "section",
+        "version": "0.6.0",
+        "_id": "/sports/lakers",
+        "_website": "latimes",
+        "name": "Lakers"
+      }
+    },
+
+    "tribune": {
+      "website_section": {
+        "type": "reference",
+        "referent": {
+          "type": "section",
+          "id": "/sports/bulls",
+          "website": "tribune"
+        }
+      }
+    },
+
+    "balsun": {
+      "website_url": "/sports/lakers-defeat-bulls"
+    }
+  },
+
+
+  "headlines": {
+    "basic": "Lakers Defeat Bulls"
+  },
+
+
+  "content_elements": [
+    {
+      "type": "text",
+      "content": "The Lakers defeated the Bulls!"
+    }
+  ],
+
+
+  "taxonomy": {
+    "primary_section": {
+      "type": "reference",
+      "referent": {
+        "type": "section",
+        "id": "/sports",
+        "website": "latimes"
+      }
+    },
+    "sections": [
+      {
+        "type": "section",
+        "version": "0.6.0",
+        "_id": "/sports/bulls",
+        "_website": "tribune",
+        "name": "Bulls",
+        "parent_id": "/sports",
+        "parent": {
+          "default": "/sports"
+        }
+      },
+      {
+        "type": "section",
+        "version": "0.6.0",
+        "_id": "/sports/lakers",
+        "_website": "latimes",
+        "name": "Lakers"
+      },
+      {
+        "type": "section",
+        "version": "0.6.0",
+        "_id": "/sports",
+        "_website": "balsun",
+        "name": "Sports"
+      }
+    ]
+  }
+}
diff --git a/tests/schema-tests-06.js b/tests/schema-tests-06.js
new file mode 100644
index 0000000..1a292af
--- /dev/null
+++ b/tests/schema-tests-06.js
@@ -0,0 +1,1094 @@
+/*global describe:true it:true */
+
+var should = require('should'),
+    dir = require('node-dir'),
+    path = require('path'),
+    Ajv = require('ajv'),
+    _ = require('lodash'),
+    ans = require('../lib/ans');
+
+var version = ans.version;
+var loadedFiles = {};
+var baseDir = path.join(path.dirname(module.filename), '../src/main/resources/schema/ans');
+var loadedSchemas = {};
+
+var ajv = new Ajv({allErrors:true});
+// var tv4 = require('tv4');
+
+var test_versions = _.map(
+  _.filter(version.history, function(item) {
+    return (item.schema == true) && (item.name.indexOf("0.6") > -1);
+  }),
+  function(item) {
+    return item.name;
+  }
+);
+
+var json_schema = {
+    "id": "http://json-schema.org/draft-04/schema#",
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "description": "Core schema meta-schema",
+    "definitions": {
+        "schemaArray": {
+            "type": "array",
+            "minItems": 1,
+            "items": { "$ref": "#" }
+        },
+        "positiveInteger": {
+            "type": "integer",
+            "minimum": 0
+        },
+        "positiveIntegerDefault0": {
+            "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
+        },
+        "simpleTypes": {
+            "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
+        },
+        "stringArray": {
+            "type": "array",
+            "items": { "type": "string" },
+            "minItems": 1,
+            "uniqueItems": true
+        }
+    },
+    "type": "object",
+    "properties": {
+        "id": {
+            "type": "string",
+            "format": "uri"
+        },
+        "$schema": {
+            "type": "string",
+            "format": "uri"
+        },
+        "title": {
+            "type": "string"
+        },
+        "description": {
+            "type": "string"
+        },
+        "default": {},
+        "multipleOf": {
+            "type": "number",
+            "minimum": 0,
+            "exclusiveMinimum": true
+        },
+        "maximum": {
+            "type": "number"
+        },
+        "exclusiveMaximum": {
+            "type": "boolean",
+            "default": false
+        },
+        "minimum": {
+            "type": "number"
+        },
+        "exclusiveMinimum": {
+            "type": "boolean",
+            "default": false
+        },
+        "maxLength": { "$ref": "#/definitions/positiveInteger" },
+        "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
+        "pattern": {
+            "type": "string",
+            "format": "regex"
+        },
+        "additionalItems": {
+            "anyOf": [
+                { "type": "boolean" },
+                { "$ref": "#" }
+            ],
+            "default": {}
+        },
+        "items": {
+            "anyOf": [
+                { "$ref": "#" },
+                { "$ref": "#/definitions/schemaArray" }
+            ],
+            "default": {}
+        },
+        "maxItems": { "$ref": "#/definitions/positiveInteger" },
+        "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
+        "uniqueItems": {
+            "type": "boolean",
+            "default": false
+        },
+        "maxProperties": { "$ref": "#/definitions/positiveInteger" },
+        "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
+        "required": { "$ref": "#/definitions/stringArray" },
+        "additionalProperties": {
+            "anyOf": [
+                { "type": "boolean" },
+                { "$ref": "#" }
+            ],
+            "default": {}
+        },
+        "definitions": {
+            "type": "object",
+            "additionalProperties": { "$ref": "#" },
+            "default": {}
+        },
+        "properties": {
+            "type": "object",
+            "additionalProperties": { "$ref": "#" },
+            "default": {}
+        },
+        "patternProperties": {
+            "type": "object",
+            "additionalProperties": { "$ref": "#" },
+            "default": {}
+        },
+        "dependencies": {
+            "type": "object",
+            "additionalProperties": {
+                "anyOf": [
+                    { "$ref": "#" },
+                    { "$ref": "#/definitions/stringArray" }
+                ]
+            }
+        },
+        "enum": {
+            "type": "array",
+            "minItems": 1,
+            "uniqueItems": true
+        },
+        "type": {
+            "anyOf": [
+                { "$ref": "#/definitions/simpleTypes" },
+                {
+                    "type": "array",
+                    "items": { "$ref": "#/definitions/simpleTypes" },
+                    "minItems": 1,
+                    "uniqueItems": true
+                }
+            ]
+        },
+        "allOf": { "$ref": "#/definitions/schemaArray" },
+        "anyOf": { "$ref": "#/definitions/schemaArray" },
+        "oneOf": { "$ref": "#/definitions/schemaArray" },
+        "not": { "$ref": "#" }
+    },
+    "dependencies": {
+        "exclusiveMaximum": [ "maximum" ],
+        "exclusiveMinimum": [ "minimum" ]
+    },
+    "default": {}
+};
+
+var hyper_schema = {
+    "$schema": "http://json-schema.org/draft-04/hyper-schema#",
+    "id": "http://json-schema.org/draft-04/hyper-schema#",
+    "title": "JSON Hyper-Schema",
+    "allOf": [
+        {
+            "$ref": "http://json-schema.org/draft-04/schema#"
+        }
+    ],
+    "properties": {
+        "additionalItems": {
+            "anyOf": [
+                {
+                    "type": "boolean"
+                },
+                {
+                    "$ref": "#"
+                }
+            ]
+        },
+        "additionalProperties": {
+            "anyOf": [
+                {
+                    "type": "boolean"
+                },
+                {
+                    "$ref": "#"
+                }
+            ]
+        },
+        "dependencies": {
+            "additionalProperties": {
+                "anyOf": [
+                    {
+                        "$ref": "#"
+                    },
+                    {
+                        "type": "array"
+                    }
+                ]
+            }
+        },
+        "items": {
+            "anyOf": [
+                {
+                    "$ref": "#"
+                },
+                {
+                    "$ref": "#/definitions/schemaArray"
+                }
+            ]
+        },
+        "definitions": {
+            "additionalProperties": {
+                "$ref": "#"
+            }
+        },
+        "patternProperties": {
+            "additionalProperties": {
+                "$ref": "#"
+            }
+        },
+        "properties": {
+            "additionalProperties": {
+                "$ref": "#"
+            }
+        },
+        "allOf": {
+            "$ref": "#/definitions/schemaArray"
+        },
+        "anyOf": {
+            "$ref": "#/definitions/schemaArray"
+        },
+        "oneOf": {
+            "$ref": "#/definitions/schemaArray"
+        },
+        "not": {
+            "$ref": "#"
+        },
+
+        "links": {
+            "type": "array",
+            "items": {
+                "$ref": "#/definitions/linkDescription"
+            }
+        },
+        "fragmentResolution": {
+            "type": "string"
+        },
+        "media": {
+            "type": "object",
+            "properties": {
+                "type": {
+                    "description": "A media type, as described in RFC 2046",
+                    "type": "string"
+                },
+                "binaryEncoding": {
+                    "description": "A content encoding scheme, as described in RFC 2045",
+                    "type": "string"
+                }
+            }
+        },
+        "pathStart": {
+            "description": "Instances' URIs must start with this value for this schema to apply to them",
+            "type": "string",
+            "format": "uri"
+        }
+    },
+    "definitions": {
+        "schemaArray": {
+            "type": "array",
+            "items": {
+                "$ref": "#"
+            }
+        },
+        "linkDescription": {
+            "title": "Link Description Object",
+            "type": "object",
+            "required": [ "href", "rel" ],
+            "properties": {
+                "href": {
+                    "description": "a URI template, as defined by RFC 6570, with the addition of the $, ( and ) characters for pre-processing",
+                    "type": "string"
+                },
+                "rel": {
+                    "description": "relation to the target resource of the link",
+                    "type": "string"
+                },
+                "title": {
+                    "description": "a title for the link",
+                    "type": "string"
+                },
+                "targetSchema": {
+                    "description": "JSON Schema describing the link target",
+                    "$ref": "#"
+                },
+                "mediaType": {
+                    "description": "media type (as defined by RFC 2046) describing the link target",
+                    "type": "string"
+                },
+                "method": {
+                    "description": "method for requesting the target of the link (e.g. for HTTP this might be \"GET\" or \"DELETE\")",
+                    "type": "string"
+                },
+                "encType": {
+                    "description": "The media type in which to submit data along with the request",
+                    "type": "string",
+                    "default": "application/json"
+                },
+                "schema": {
+                    "description": "Schema describing the data to submit along with the request",
+                    "$ref": "#"
+                }
+            }
+        }
+    },
+    "links": [
+        {
+            "rel": "self",
+            "href": "{+id}"
+        },
+        {
+            "rel": "full",
+            "href": "{+($ref)}"
+        }
+    ]
+};
+
+
+describe("ANS Schema", function() {
+  beforeEach(function(done) {
+    ans.getSchemas(function(err, schemas) {
+      loadedSchemas = schemas;
+      done();
+    });
+  });
+  describe("All schemas can be loaded as JSON Schema", function() {
+    it("should load all schemas into an object", function(done) {
+
+      dir.readFiles(
+        baseDir, {
+          match:  /.json$/
+        },
+        function(err, content, filename, next) {
+          if (err) throw err;
+          var name = path.relative(baseDir, filename);
+          loadedFiles[name] = content;
+          next();
+        },
+        function(err, files) {
+          if (err) {
+            console.log(err);
+            throw err;
+          }
+
+          var keys = Object.keys(loadedFiles);
+          for ( var i = 0; i < keys.length; i++) {
+
+            var schema = loadedSchemas[keys[i]];
+
+            schema.should.be.instanceOf(Object);
+            schema.should.deepEqual(JSON.parse(loadedFiles[keys[i]]));
+          }
+          done();
+        }
+      );
+    });
+
+    it("should validate as valid JSON Schema and Hyper Schema", function() {
+      var keys = Object.keys(loadedSchemas);
+
+      for( var i = 0; i < keys.length; i++) {
+        console.log("        " + keys[i] + " should be a valid JSON Schema");
+        var schema = loadedSchemas[keys[i]];
+        ajv.addSchema(schema);
+
+        // Uncomment to validate with tv4
+        //tv4.addSchema(schema);
+      }
+
+    });
+  });
+});
+
+
+
+var fixtures = {};
+var validate = function(version, schemaName, fixture, expected) {
+  //console.log("schema: " + schemaName);
+  //console.log("fixture: " + fixture);
+
+  var schema = loadedSchemas[version + schemaName];
+  fixture = (typeof fixture === "string") ? fixtures[fixture] : fixture;
+
+  expected = (typeof expected === "undefined") ? true : expected;
+
+  var result = ajv.validate(schema, fixture);
+  if (result !== expected) {
+    console.log(JSON.stringify(fixture));
+    console.log(JSON.stringify(schema));
+    console.log(ajv.errors);
+  }
+  result.should.eql(expected);
+
+  // Uncomment to validate with tv4
+  // var result = tv4.validateResult(fixture, schema, false, false);
+  // if (result.valid != expected) {
+  //   console.log(fixtureName);
+  //   console.log(result);
+  // }
+  // result.valid.should.eql(expected);
+
+};
+
+var validateIfFixtureExists = function(version, schemaName, fixture, expected) {
+  if(_.has(fixtures, fixture) && _.has(loadedSchemas, version + schemaName)) {
+    validate(version, schemaName, fixture, expected);
+  }
+};
+
+
+describe("Schema: ", function() {
+
+  test_versions.forEach(function(version) {
+
+    var versionObj = new ans.version.Version(ans.version.parse_version(version));
+
+    describe("ANS ", function() {
+
+      before(function(done) {
+        fixtures = {};
+        dir.readFiles(
+          path.join(path.dirname(module.filename), 'fixtures/schema/' + version),
+          function(err, content, filename, next) {
+            if (err) throw err;
+
+            try {
+              fixtures[path.basename(filename, '.json')] = JSON.parse(content);
+            }
+            catch (err) {
+              console.log("Could not load " + filename);
+            }
+            next();
+          },
+          function(err, files) {
+            if (err) throw err;
+            done();
+          }
+        );
+      });
+
+      describe(version, function() {
+        describe("Author (0.5.9+)", function() {
+          it("should validate a well-formatted author", function() {
+            validateIfFixtureExists('/utils/author.json', 'author-fixture-good');
+          });
+        });
+
+        describe("Audio", function() {
+          it("should validate a well-formatted audio", function() {
+            validate(version, '/audio.json', 'audio-fixture-good');
+          });
+
+          it("should validate audio with streams (0.5.9+)", function() {
+            validateIfFixtureExists(version, '/audio.json', 'audio-fixture-good-streams');
+          });
+
+          it("should validate a well-formatted audio with settings", function() {
+            validate(version, '/audio.json', 'audio-fixture-good-settings');
+          });
+
+          it("should validate a well-formatted audio with custom fields", function() {
+            validate(version, '/audio.json', 'audio-fixture-good-custom');
+          });
+
+          it("should not validate a non-audio element", function() {
+            validate(version, '/audio.json', 'audio-fixture-bad', false);
+          });
+
+          it("should not validate an audio with no source (0.5.9+)", function() {
+            validateIfFixtureExists(version, '/audio.json', 'audio-fixture-bad-no-source', false);
+          });
+
+          it("should validate as content", function() {
+            validate(version, '/content.json', 'audio-fixture-good')
+            validate(version, '/content.json', 'audio-fixture-good-settings')
+            validate(version, '/content.json', 'audio-fixture-good-custom')
+          });
+        });
+
+        describe("Video", function() {
+          it("should validate a well-formatted video", function() {
+            validate(version, '/video.json', 'video-fixture-good');
+            validate(version, '/video.json', 'video-fixture-nationals');
+          });
+
+          it("should not validate a non-well-formatted video", function() {
+            validate(version, '/video.json', 'video-fixture-bad', false);
+          });
+
+          it("should validate as content", function() {
+            validate(version, '/content.json', 'video-fixture-good');
+          });
+        });
+
+        describe("Story", function() {
+          it("should validate a story", function() {
+            validate(version, '/story.json', 'story-fixture-good');
+            validate(version, '/story.json', 'story-fixture-tiny-house');
+            validate(version, '/story.json', 'story-fixture-references');
+            validate(version, '/story.json', 'story-fixture-good-multi-site');
+          });
+
+          it("should validate a story with a content element with unknown properties", function() {
+            validate(version, '/story.json', 'story-fixture-good-mystery-element');
+          });
+
+          it("should not validate a story with unspecified properties", function() {
+            validate(version, '/story.json', 'story-fixture-bad-extra-properties', false);
+          });
+
+          it("should not validate a story with the wrong type", function() {
+            validate(version, '/story.json', 'story-fixture-bad-wrong-type', false);
+          });
+
+          it("should not validate a story with the wrong version", function() {
+            validate(version, '/story.json', 'story-fixture-bad-wrong-version', false);
+          });
+
+          it("should not validate a story with bad taxonomy (0.5.3+)", function() {
+            validateIfFixtureExists(version, '/story.json', 'story-fixture-bad-tag-strings', false);
+            validateIfFixtureExists(version, '/story.json', 'story-fixture-bad-sections-addl-properties', false);
+          });
+
+          it("should validate as content", function() {
+            validate(version, '/content.json', 'story-fixture-good');
+            validate(version, '/content.json', 'story-fixture-tiny-house');
+            validate(version, '/content.json', 'story-fixture-references');
+          });
+
+          it("should not validate a story with bad corrections", function() {
+              validate(version, '/story.json', 'story-fixture-bad-corrections', false);
+          });
+
+          it("should not validate a story with bad interstitial link", function() {
+              validate(version, '/story.json', 'story-fixture-bad-interstitial-link', false);
+          });
+
+        });
+
+        describe("Image", function() {
+          it("should validate a valid image", function() {
+            validate(version, '/image.json', 'image-fixture-good');
+          });
+          it("should validate an image with no height or width", function() {
+            validate(version, '/image.json', 'image-fixture-good-no-height-width');
+          });
+
+          it("should validate as content", function() {
+            validate(version, '/content.json', 'image-fixture-good');
+          });
+        });
+
+        describe("Image Operation", function() {
+          var type = "/image_operation.json";
+          ['create', 'update', 'delete'].forEach(function (op) {
+            it("should validate image " + op + " operation", function () {
+              validateIfFixtureExists(version, type, "image-operation-" + op);
+            });
+          });
+        });
+
+        describe("Gallery", function() {
+          it("should validate a valid gallery", function() {
+            validateIfFixtureExists(version, '/gallery.json', 'gallery-fixture-good');
+          });
+          it("should validate a gallery with no images", function() {
+            validateIfFixtureExists(version, '/gallery.json', 'gallery-fixture-good-no-images');
+          });
+          it("should validate as content", function() {
+            validateIfFixtureExists(version, '/content.json', 'gallery-fixture-good');
+          });
+        });
+
+        describe("Gallery Operation", function() {
+          var type = "/gallery_operation.json";
+          ['create', 'update', 'delete'].forEach(function (op) {
+            it("should validate gallery " + op + " operation", function () {
+              validateIfFixtureExists(version, type, "gallery-operation-" + op);
+            });
+          });
+        });
+
+        describe("Alignment", function() {
+          it("should not validate an object with an invalid alignment setting", function() {
+            validateIfFixtureExists(version, '/image.json', 'image-fixture-bad-alignment', false);
+          });
+        });
+
+        describe("Redirect", function() {
+            it("should validate a valid redirect", function() {
+                validateIfFixtureExists(version, '/redirect.json', 'redirect-fixture-good');
+            });
+            it("should not validate an invalid redirect", function() {
+                validateIfFixtureExists(version, '/redirect.json', 'redirect-fixture-bad', false);
+            });
+        });
+
+        describe("URL Operation", function() {
+          var type = "/url_operation.json";
+          ['insert-redirect'].forEach(function (op) {
+              it("should validate url " + op + " operation", function () {
+                  validateIfFixtureExists(version, type, "url-operation-" + op);
+              });
+          });
+        });
+
+        describe("Content Operation", function() {
+          var type = "/content_operation.json";
+          it("should validate a referent update", function () {
+            validateIfFixtureExists(version, type, "content-operation-good");
+          });
+        });
+
+        describe("Story Operation", function() {
+          var type = "/story_operation.json";
+
+          it("should validate a create operation", function() {
+            validate(version, type, 'operation-create');
+          });
+          it("should validate an update operation", function() {
+            validate(version, type, 'operation-update');
+          });
+          it("should validate a delete operation", function() {
+            validate(version, type, 'operation-delete');
+          });
+          it("should validate a publish-edition operation", function() {
+            validate(version, type, 'operation-publish-edition');
+          });
+          it("should validate an unpublish-edition operation", function() {
+            validate(version, type, 'operation-unpublish-edition');
+          });
+        });
+
+        describe("Reference", function() {
+          it("should validate a reference", function() {
+            validate(version, '/utils/reference.json', 'reference-fixture-good');
+          });
+
+          it("should not validate a reference with additional properties (0.5.4+)", function() {
+            validateIfFixtureExists(version, '/utils/reference.json', 'reference-fixture-bad-addl-props', false);
+          });
+
+          it("should not validate a reference with a referent with additional properties (0.5.4+)", function() {
+            validateIfFixtureExists(version, '/utils/reference.json', 'reference-fixture-bad-more-addl-props', false);
+          });
+
+          it("should not validate an array as a reference (0.5.8+)", function() {
+            validateIfFixtureExists(version, '/utils/reference.json', 'reference-fixture-bad-array', false);
+          });
+        });
+
+        describe("Clavis (0.5.5+)", function() {
+
+          describe("Clavis Keywords", function() {
+            it("should validate a keyword", function() {
+              validateIfFixtureExists(version, '/utils/keyword.json', 'keyword-fixture-good');
+            });
+
+            it("should not validate a keyword with a bad score", function() {
+              validateIfFixtureExists(version, '/utils/keyword.json', 'keyword-fixture-bad-score-type', false);
+            });
+
+            it("should not validate a keyword with a missing score", function() {
+              validateIfFixtureExists(version, '/utils/keyword.json', 'keyword-fixture-bad-missing-score', false);
+            });
+
+            it("should not validate a keyword with a bad tag type", function() {
+              validateIfFixtureExists(version, '/utils/keyword.json', 'keyword-fixture-bad-tag-type', false);
+            });
+
+            it("should not validate a keyword with a bad frequency type", function() {
+              validateIfFixtureExists(version, '/utils/keyword.json', 'keyword-fixture-bad-numeric-frequency', false);
+            });
+          });
+
+          describe("Clavis Topics", function() {
+            it("should validate a topic", function() {
+              validateIfFixtureExists(version, '/utils/topic.json', 'topic-fixture-good');
+            });
+
+            it("should validate a topic with a missing name", function() {
+              validateIfFixtureExists(version, '/utils/topic.json', 'topic-fixture-good-missing-name');
+            });
+
+            it("should not validate a topic with a bad id", function() {
+              validateIfFixtureExists(version, '/utils/topic.json', 'toppic-fixture-bad-id-type', false);
+            });
+
+            it("should not validate a topic with a missing uid", function() {
+              validateIfFixtureExists(version, '/utils/topic.json', 'topic-fixture-bad-missing-uid', false);
+            });
+          });
+
+          describe("Clavis Auxiliaries", function() {
+            it("should validate an auxiliary", function() {
+              validateIfFixtureExists(version, '/utils/auxiliary.json', 'auxiliary-fixture-good');
+            });
+
+            it("should validate an auxiliary without a name", function() {
+              validateIfFixtureExists(version, '/utils/auxiliary.json', 'auxiliary-fixture-good-optional-name');
+            });
+
+            it("should not validate an auxiliary missing an uid", function() {
+              validateIfFixtureExists(version, '/utils/auxiliary.json', 'auxiliary-fixture-bad-missing-uid', false);
+            });
+          });
+
+          describe("Named Entities", function() {
+            it("should validate a named entity", function() {
+              validateIfFixtureExists(version, '/utils/named_entity.json', 'named-entity-fixture-good');
+            });
+
+            it("should not validate a named entity without id", function() {
+              validateIfFixtureExists(version, '/utils/named_entity.json', 'named-entity-fixture-bad-missing-id', false);
+            });
+          });
+
+        });
+
+        describe("Label (0.5.8+)", function() {
+          it("should validate a valid label", function() {
+            validateIfFixtureExists(version, '/traits/trait_label.json', 'label-good');
+          });
+
+          it("should not validate a label with extra properties", function() {
+            validateIfFixtureExists(version, '/traits/trait_label.json', 'label-bad-arbitrary-properties', false);
+          });
+
+          it("should not validate a valid label with fields with extra characters", function() {
+            validateIfFixtureExists(version, '/traits/trait_label.json', 'label-bad-field-name-with-dots', false);
+          });
+        });
+
+
+        describe("Source (0.5.8+)", function() {
+          it("should validate a valid source", function() {
+            validateIfFixtureExists(version, '/traits/trait_source.json', 'source-good');
+          });
+        });
+
+        describe("Pitches (0.5.8+)", function() {
+          it("should validate properly formatted pitches", function() {
+            validateIfFixtureExists(version, '/traits/trait_pitches.json', 'trait-pitches-fixture-good', true);
+          });
+          it("should reject platform pitch event status containing numeric characters", function() {
+            validateIfFixtureExists(version, '/traits/trait_platform_pitch_event.json', 'trait-pitch-event-fixture-numeric-status', false);
+          });
+          it("should reject platform pitch event status containing uppercase characters", function() {
+            validateIfFixtureExists(version, '/traits/trait_platform_pitch_event.json', 'trait-pitch-event-fixture-uppercase-status', false);
+          });
+          it("should allow platform pitch event status containing spaces", function() {
+            validateIfFixtureExists(version, '/traits/trait_platform_pitch_event.json', 'trait-pitch-event-fixture-space-in-status', true);
+          });
+          it("should reject publication pitch event status containing numeric characters", function() {
+            validateIfFixtureExists(version, '/traits/trait_publication_pitch_event.json', 'trait-pitch-event-fixture-numeric-status', false);
+          });
+          it("should reject publication pitch event status containing uppercase characters", function() {
+            validateIfFixtureExists(version, '/traits/trait_publication_pitch_event.json', 'trait-pitch-event-fixture-uppercase-status', false);
+          });
+          it("should allow publication pitch event status containing spaces", function() {
+            validateIfFixtureExists(version, '/traits/trait_publication_pitch_event.json', 'trait-pitch-event-fixture-space-in-status', true);
+          });
+        });
+
+        describe("Workflow (0.5.8+)", function() {
+          it("should validate properly formatted workflow information", function() {
+            validateIfFixtureExists(version, '/traits/trait_workflow.json', 'trait-workflow-fixture-good', true);
+          });
+          it("should reject invalid workflow status code", function() {
+            validateIfFixtureExists(version, '/traits/trait_workflow.json', 'trait-workflow-fixture-bad-code', false);
+          });
+        });
+
+        describe("Planning (0.5.8+)", function() {
+          it("should validate properly formatted planning information", function() {
+            validateIfFixtureExists(version, '/traits/trait_planning.json', 'trait-planning-fixture-good', true);
+          });
+        });
+
+        describe("Publishing (0.5.8+)", function() {
+          it("should validate publishing information", function() {
+            validateIfFixtureExists(version, '/traits/trait_publishing.json', 'trait-publishing-fixture-good', true);
+          })
+
+          it('should not validate publishing with incorrect story operation types', function() {
+            validateIfFixtureExists(version, '/traits/trait_publishing.json', 'trait-publishing-fixture-bad-mismatched-operations', false);
+          });
+        });
+
+        describe("Related Content (0.5.8+)", function() {
+          it("should not validate related_content.redirect that is not a redirect or that has too many items", function() {
+            validateIfFixtureExists(version, '/traits/trait_related_content.json', 'trait-related-content-bad-attached-redirect-has-multiple-items', false);
+            validateIfFixtureExists(version, '/traits/trait_related_content.json', 'trait-related-content-bad-attached-redirect-is-not-redirect', false);
+          });
+        });
+
+        describe("Story Elements ", function() {
+          var type_prefix = "/story_elements";
+
+          describe("Blockquote", function() {
+            it("should validate a well-formatted blockquote", function() {
+              validate(version, type_prefix + '/blockquote.json', 'bq-fixture-good');
+            });
+
+            it("should not validate a non-blockquote", function() {
+              validate(version, type_prefix + '/blockquote.json', 'bq-fixture-bad', false);
+            });
+          });
+
+          describe("Code", function() {
+            it("should validate a well-formatted code sample", function() {
+              validate(version, type_prefix + '/code.json', 'code-fixture-good');
+            });
+          });
+
+          describe("Numeric Rating", function() {
+            it("should validate a valid numeric rating", function() {
+              validateIfFixtureExists(version, type_prefix + '/numeric_rating.json', 'numeric-rating-fixture-good');
+            });
+
+            it("should not validate an invalid numeric rating", function() {
+              validateIfFixtureExists(version, type_prefix + '/numeric_rating.json', 'numeric-rating-fixture-bad', false);
+            });
+
+            it("should validate a numeric rating with no min, max, or units", function() {
+               validateIfFixtureExists(version, type_prefix + '/numeric_rating.json', 'numeric-rating-fixture-good-only-rating');
+            });
+          });
+
+          describe("Endorsement", function() {
+            it("should validate a valid endorsement", function() {
+              validateIfFixtureExists(version, type_prefix + '/endorsement.json', 'endorsement-fixture-good');
+            });
+
+            it("should not validate an endorsement with a number value for a property that should be a string", function() {
+              validateIfFixtureExists(version, type_prefix + '/endorsement.json', 'endorsement-fixture-bad-type-mismatch', false);
+            });
+
+            it("should not validate an endorsement with a property missing", function() {
+              validateIfFixtureExists(version, type_prefix + '/endorsement.json', 'endorsement-fixture-bad-missing-prop', false);
+            });
+          });
+
+          describe("List", function() {
+            it("should validate a list of text elements", function() {
+              validate(version, type_prefix + '/list.json', 'ul-fixture-good');
+              validate(version, type_prefix + '/list_element.json', 'ul-fixture-good');
+            });
+
+            it("should validate a nested list of text elements", function() {
+              validate(version, type_prefix + '/list.json', 'ul-fixture-good-nested');
+            });
+          });
+
+          if (versionObj.lt(new ans.version.Version(ans.version.parse_version("0.5.7")))) {
+            describe("Oembed", function() {
+              it("should validate an oembed element", function() {
+                validate(version, type_prefix + '/oembed.json', 'oembed-fixture-good');
+              });
+
+              it("should not validate a non-oembed", function() {
+                validate(version, type_prefix + '/oembed.json', 'oembed-fixture-bad', false);
+              });
+            });
+          }
+
+          if (versionObj.gt(new ans.version.Version(ans.version.parse_version("0.5.6")))) {
+            describe("Oembed Response", function() {
+              it("should validate an oembed response object", function() {
+                validate(version, "/utils/oembed_response.json", "oembed-response-fixture-good");
+              });
+            });
+          }
+
+          describe("Text", function() {
+            it("should validate a text element", function() {
+              validate(version, type_prefix + '/text.json', 'text-fixture-good');
+            });
+
+            it("should validate a text element with channels", function() {
+              validate(version, type_prefix + '/text.json', 'text-fixture-good-channels');
+            });
+
+            it("should not validate a non-text", function() {
+              validate(version, type_prefix + '/text.json', 'text-fixture-bad', false);
+            });
+
+            it("should validate a text element with additional properties (0.5.1+)", function() {
+              if ('text-fixture-good-additional-properties' in fixtures) {
+                validate(version, type_prefix + '/text.json', 'text-fixture-good-additional-properties');
+              };
+            });
+
+          });
+
+          describe("Raw Html", function() {
+            it("should validate a raw_html element", function() {
+              validate(version, type_prefix + '/raw_html.json', 'raw-html-fixture-good');
+            });
+
+            it("should not validate a non-raw_html", function() {
+              validate(version, type_prefix + '/raw_html.json', 'raw-html-fixture-bad', false);
+            });
+
+          });
+
+          describe("Header", function() {
+            it("should validate a header element", function() {
+              validate(version, type_prefix + '/header.json', 'header-fixture-good');
+            });
+
+            it("should not validate a header with 'text' property", function() {
+              validate(version, type_prefix + '/header.json', 'header-fixture-bad', false);
+            });
+          });
+
+
+          describe("Table", function() {
+            it("should validate a table element", function() {
+              validate(version, type_prefix + '/table.json', 'table-fixture-good');
+            });
+
+            it("should not validate a non-table", function() {
+              validate(version, type_prefix + '/table.json', 'raw-html-fixture-bad', false);
+            });
+          });
+
+          describe("Interstitial Link", function() {
+            it("should validate a interstitial link", function () {
+              validateIfFixtureExists(version, type_prefix + '/interstitial_link.json', 'interstitial-link-fixture-good');
+            });
+
+            it("should not validate a non-interstitial link", function () {
+              validateIfFixtureExists(version, type_prefix + '/interstitial_link.json', 'interstitial-link-fixture-bad', false);
+            });
+          });
+
+          describe("Element Group (0.5.8+)", function() {
+            it("should validate an element group", function() {
+              validateIfFixtureExists(version, type_prefix + '/element_group.json', 'element-group-fixture-good');
+            });
+          });
+
+          describe("Quote (0.5.8+)", function() {
+            it("should not validate a quote with an invalid citation", function() {
+              validateIfFixtureExists(version, type_prefix + '/quote.json', 'quote-fixture-bad-citation', false);
+            });
+            it("should not validate a quote with no content elements", function() {
+              validateIfFixtureExists(version, type_prefix + '/quote.json', 'quote-fixture-bad-content-elements', false);
+            });
+          });
+
+
+          describe("...all together now", function() {
+            var valid_fixtures = [ "story-fixture-good", "story-fixture-references" ];
+            valid_fixtures.forEach(function(fixtureName) {
+
+              it("should validate every content element in a valid story: " + fixtureName, function() {
+                var document = fixtures[fixtureName];
+
+                document.content_elements.forEach(function(element) {
+
+                  element.type.should.equalOneOf([ "blockquote", "code", "interstitial_link", "list", "oembed", "oembed_response", "raw_html", "table", "text", "reference", "image", "video", "audio", "story", "element_group", "quote", "correction", "numeric_rating", "endorsement"]);
+
+                  switch(element.type) {
+                  case "correction":
+                    validate(version, type_prefix + '/correction.json', element);
+                    break;
+                  case "quote":
+                    validate(version, type_prefix + '/quote.json', element);
+                    break;
+                  case "blockquote":
+                    validate(version, type_prefix + '/blockquote.json', element);
+                    break;
+                  case "code":
+                    validate(version, type_prefix + '/code.json', element);
+                    break;
+                  case "list":
+                    validate(version, type_prefix + '/list.json', element);
+                    break;
+                  case "oembed":
+                    validate(version, type_prefix + '/oembed.json', element);
+                    break;
+                  case "oembed_response":
+                    // Oembed format changed in 0.5.7
+                    validate(version, '/utils/oembed_response.json', element);
+                    break;
+                  case "raw_html":
+                    validate(version, type_prefix + '/raw_html.json', element);
+                    break;
+                  case "table":
+                    validate(version, type_prefix + '/table.json', element);
+                    break;
+                  case "text":
+                    validate(version, type_prefix + '/text.json', element);
+                    break;
+                  case "reference":
+                    validate(version, '/utils/reference.json', element);
+                    break;
+                  case "image":
+                    validate(version, '/image.json', element);
+                    break;
+                  case "video":
+                    validate(version, '/video.json', element);
+                    break;
+                  case "audio":
+                    validate(version, '/audio.json', element);
+                    break;
+                  case "story":
+                    validate(version, '/story.json', element);
+                    break;
+                  case "numeric_rating":
+                    validate(version, type_prefix + '/numeric_rating.json', element);
+                    break;
+                  case "endorsement":
+                    validate(version, type_prefix + '/endorsement.json', element);
+                    break;
+                  }
+                });
+              });
+
+              it("should validate every promo item in a valid story: " + fixtureName, function() {
+                var document = fixtures[fixtureName];
+
+                Object.keys(document.promo_items).forEach(function(item_name) {
+                  var item = document.promo_items[item_name];
+                  item.type.should.equalOneOf([ "image", "video", "audio", "story", "reference" ]);
+
+                  switch(item.type) {
+                  case "image":
+                    validate(version, '/image.json', item);
+                    break;
+                  case "video":
+                    validate(version, '/video.json', item);
+                    break;
+                  case "audio":
+                    validate(version, '/audio.json', item);
+                    break;
+                  case "story":
+                    validate(version, '/story.json', item);
+                    break;
+                  case "reference":
+                    validate(version, '/utils/reference.json', item);
+                    break;
+                  }
+                });
+              });
+            });
+          });
+        });
+
+        describe("Misc bug fixes", function() {
+
+          describe("Promo Items", function() {
+            it("should not validate without a 'basic' key (0.5.3+)", function() {
+              if('promo-items-bad-no-basic' in fixtures) {
+                validate(version, '/traits/trait_promo_items.json', 'promo-items-bad-no-basic', false);
+              }
+            });
+          });
+        });
+
+      });
+    });
+  });
+});