Publish Manifest Schema Specification
Document Overview
Purpose: This document defines the formal JSON Schema for the publish.json manifest file, which serves as the central metadata registry for all documents, dashboards, and session logs in the CODITECT Bioscience QMS Platform. The manifest enables document discovery, search indexing, access control, and publication workflows.
Scope: This specification covers:
- Complete JSON Schema definition (Draft 2020-12)
- Field-by-field specifications with types, constraints, and examples
- Validation rules including referential integrity
- CLI validation tooling (
npm run validate-manifest) - Error handling and reporting formats
- Schema versioning and migration strategy
- Build pipeline integration
Target Audience: Platform developers, DevOps engineers, technical documentation teams, and compliance auditors.
Related Documents:
- ADR-195: Push-Button Documentation Publishing (Cloud Publishing Platform)
scripts/generate-publish-manifest.js: Manifest generation implementationpublic/publish.json: Production manifest file (133+ documents)
Table of Contents
- Schema Definition
- Field Specifications
- Validation Rules
- CLI Validation Command
- Error Handling
- Schema Versioning
- Build Pipeline Integration
- Examples
- Migration Guide
- Appendices
Schema Definition
JSON Schema (Draft 2020-12)
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://coditect.ai/schemas/publish-manifest/v1.0.0",
"title": "CODITECT Publish Manifest",
"description": "Schema for publish.json manifest file — document metadata registry for the CODITECT Bioscience QMS Platform",
"type": "object",
"required": [
"project_name",
"version",
"generated_at",
"total_documents",
"categories",
"documents"
],
"properties": {
"project_name": {
"type": "string",
"description": "Human-readable project name",
"minLength": 1,
"maxLength": 200,
"examples": ["CODITECT Bioscience QMS Platform"]
},
"version": {
"type": "string",
"description": "Semantic version of the manifest schema and project",
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$",
"examples": ["1.0.0", "1.2.3-beta.1", "2.0.0+build.456"]
},
"generated_at": {
"type": "string",
"description": "ISO 8601 timestamp when the manifest was generated",
"format": "date-time",
"examples": ["2026-02-16T04:29:25.849Z"]
},
"base_url": {
"type": "string",
"description": "Optional base URL for resolving relative document paths (production deployment URL)",
"format": "uri",
"examples": ["https://qms.coditect.ai", "https://docs.example.com"]
},
"auth_mode": {
"type": "string",
"description": "Authentication mode for document access control",
"enum": ["public", "authenticated", "role-based", "sso", "none"],
"default": "none",
"examples": ["role-based"]
},
"total_documents": {
"type": "integer",
"description": "Total count of documents in the manifest (must equal documents.length)",
"minimum": 0,
"examples": [133]
},
"categories": {
"type": "array",
"description": "Array of document categories with type breakdowns",
"minItems": 0,
"items": {
"$ref": "#/$defs/Category"
}
},
"audiences": {
"type": "array",
"description": "Optional array of defined audience types for access control",
"items": {
"type": "string",
"minLength": 1,
"examples": ["technical", "business", "executive", "contributor", "compliance"]
},
"uniqueItems": true
},
"documents": {
"type": "array",
"description": "Array of all document metadata entries",
"minItems": 0,
"items": {
"$ref": "#/$defs/Document"
}
}
},
"additionalProperties": false,
"$defs": {
"Category": {
"type": "object",
"description": "Category summary with document counts and type distribution",
"required": ["name", "count", "types"],
"properties": {
"name": {
"type": "string",
"description": "Category name (must match at least one document's category field)",
"minLength": 1,
"maxLength": 100,
"examples": ["Architecture", "Compliance", "Executive"]
},
"count": {
"type": "integer",
"description": "Number of documents in this category",
"minimum": 1
},
"types": {
"type": "object",
"description": "Document type breakdown (type → count mapping)",
"patternProperties": {
"^(markdown|dashboard|pdf|html|docx)$": {
"type": "integer",
"minimum": 1
}
},
"additionalProperties": false,
"minProperties": 1,
"examples": [
{"markdown": 6},
{"markdown": 6, "dashboard": 4}
]
}
},
"additionalProperties": false
},
"Document": {
"type": "object",
"description": "Metadata for a single document (markdown, dashboard, or other type)",
"required": ["id", "title", "path", "type", "category"],
"properties": {
"id": {
"type": "string",
"description": "Unique document identifier (derived from path, must be URL-safe)",
"pattern": "^[a-zA-Z0-9._-]+$",
"minLength": 1,
"maxLength": 500,
"examples": [
"docs-architecture-system-design.md",
"dashboards-business-revenue-forecast"
]
},
"title": {
"type": "string",
"description": "Human-readable document title (from frontmatter or filename)",
"minLength": 1,
"maxLength": 500,
"examples": ["System Design Document", "Revenue Forecast Dashboard"]
},
"path": {
"type": "string",
"description": "Relative path from project root to the document file",
"minLength": 1,
"maxLength": 1000,
"examples": [
"docs/architecture/system-design.md",
"dashboards/business/revenue-forecast.jsx",
"session-logs/SESSION-LOG-2026-02-16.md"
]
},
"type": {
"type": "string",
"description": "Document type (determines renderer and indexing strategy)",
"enum": ["markdown", "dashboard", "pdf", "html", "docx"],
"examples": ["markdown", "dashboard"]
},
"audience": {
"type": "string",
"description": "Target audience for access control and filtering",
"default": "technical",
"examples": ["technical", "business", "executive", "contributor", "compliance"]
},
"category": {
"type": "string",
"description": "Document category (must match a category in top-level categories array)",
"minLength": 1,
"maxLength": 100,
"examples": ["Architecture", "Compliance", "Executive", "Session Logs"]
},
"keywords": {
"type": "array",
"description": "Keywords for search indexing (from frontmatter or auto-extracted)",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"uniqueItems": true,
"examples": [
["architecture", "system-design", "microservices"],
["revenue", "forecast", "business-metrics"]
]
},
"summary": {
"type": "string",
"description": "Brief summary or description (from frontmatter)",
"maxLength": 1000,
"examples": ["High-level system architecture and component design for the QMS platform."]
},
"author": {
"type": "string",
"description": "Document author (from frontmatter)",
"maxLength": 200,
"examples": ["Hal Casteel", "Claude (Opus 4.6)"]
},
"status": {
"type": "string",
"description": "Document lifecycle status",
"enum": ["draft", "active", "archived", "deprecated"],
"default": "active",
"examples": ["active", "draft"]
},
"body_text": {
"type": "string",
"description": "Searchable plain-text version of document content (markdown formatting stripped)",
"maxLength": 1000000,
"examples": ["System Design Document The CODITECT platform uses..."]
}
},
"additionalProperties": false
}
}
}
Schema File Location
The schema file is stored at:
config/schemas/publish-manifest-v1.0.0.schema.json
Symlink for latest version:
ln -s publish-manifest-v1.0.0.schema.json config/schemas/publish-manifest.schema.json
Field Specifications
Top-Level Fields
project_name (required)
- Type:
string - Constraints: 1-200 characters
- Description: Human-readable project name displayed in the UI and metadata.
- Example:
"CODITECT Bioscience QMS Platform" - Validation: Non-empty string, no special format requirements.
version (required)
- Type:
string - Constraints: Must match semantic versioning pattern (SemVer 2.0.0).
- Pattern:
^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ - Description: Version of the manifest schema and optionally the project/documentation bundle. Incremented on schema changes or major documentation updates.
- Examples:
"1.0.0"— Initial release"1.2.3-beta.1"— Pre-release version"2.0.0+build.456"— Build metadata included
- Validation: Regex validation against SemVer pattern.
generated_at (required)
- Type:
string(ISO 8601 date-time) - Format:
date-time - Description: UTC timestamp when the manifest was generated. Used for cache invalidation and freshness checks.
- Example:
"2026-02-16T04:29:25.849Z" - Validation: Must be a valid ISO 8601 date-time string with timezone.
base_url (optional)
- Type:
string(URI) - Format:
uri - Description: Base URL for resolving relative document paths in production deployments. Used to construct absolute URLs for sharing and SEO.
- Examples:
"https://qms.coditect.ai""https://docs.example.com"
- Validation: Must be a valid absolute URI with scheme (http/https).
auth_mode (optional)
- Type:
string(enum) - Allowed Values:
"public"— All documents accessible without authentication"authenticated"— Requires login, no role checks"role-based"— Audience-based access control (maps document.audience to user roles)"sso"— Single Sign-On integration"none"— No authentication (default)
- Default:
"none" - Description: Authentication mode for the publishing platform. Determines access control logic for document retrieval.
- Example:
"role-based" - Validation: Must be one of the enum values.
total_documents (required)
- Type:
integer - Constraints:
>= 0, must equaldocuments.length - Description: Total count of documents in the manifest. Used for validation and UI display.
- Example:
133 - Validation: Must match the actual count of items in the
documentsarray.
categories (required)
- Type:
arrayofCategoryobjects - Description: Summary of document categories with counts and type breakdowns. Used for navigation and filtering.
- Example:
[
{
"name": "Architecture",
"count": 6,
"types": {"markdown": 6}
},
{
"name": "Compliance",
"count": 10,
"types": {"markdown": 6, "dashboard": 4}
}
] - Validation:
- All category names must have at least one matching document
- Counts must sum correctly (category.count = sum of type counts)
- Type counts must match actual document counts by category+type
audiences (optional)
- Type:
arrayofstring - Constraints: Unique items, non-empty strings
- Description: Enumeration of valid audience types for the project. If present, all
document.audiencevalues must be in this list. - Examples:
["technical", "business", "executive", "contributor", "compliance"] - Validation: Must be unique, each item >= 1 character.
documents (required)
- Type:
arrayofDocumentobjects - Description: Array of all document metadata entries. This is the primary data payload of the manifest.
- Validation:
- All document IDs must be unique
- All document categories must exist in the
categoriesarray - If
audiencesis defined, alldocument.audiencevalues must be in that list - Length must equal
total_documents
Category Object
name (required)
- Type:
string - Constraints: 1-100 characters
- Description: Category name. Must match at least one document's
categoryfield. - Examples:
"Architecture","Compliance","Executive"
count (required)
- Type:
integer - Constraints:
>= 1 - Description: Total number of documents in this category. Must equal the sum of all type counts.
- Example:
10
types (required)
- Type:
object(type → count mapping) - Allowed Keys:
markdown,dashboard,pdf,html,docx - Description: Breakdown of document types within the category. Each key is a document type, each value is the count.
- Example:
{
"markdown": 6,
"dashboard": 4
} - Validation:
- Must have at least one type entry
- All counts must be
>= 1 - Sum of counts must equal
category.count
Document Object
id (required)
- Type:
string - Pattern:
^[a-zA-Z0-9._-]+$(URL-safe characters only) - Constraints: 1-500 characters, unique across all documents
- Description: Unique identifier derived from the document path. Used for routing and fragment identifiers.
- Generation Logic:
path.replace(/[/\\]/g, "-").replace(/\.(md|jsx)$/, "") - Examples:
"docs-architecture-system-design""dashboards-business-revenue-forecast""session-logs-SESSION-LOG-2026-02-16"
title (required)
- Type:
string - Constraints: 1-500 characters
- Description: Human-readable document title. Extracted from frontmatter
titlefield or generated from filename. - Examples:
"System Design Document","Revenue Forecast Dashboard"
path (required)
- Type:
string - Constraints: 1-1000 characters
- Description: Relative path from project root to the document file. Used for file retrieval and routing.
- Examples:
"docs/architecture/system-design.md""dashboards/business/revenue-forecast.jsx""session-logs/SESSION-LOG-2026-02-16.md"
- Validation: Path must exist on filesystem at build time (enforced by generation script, not schema).
type (required)
- Type:
string(enum) - Allowed Values:
markdown,dashboard,pdf,html,docx - Description: Document type. Determines which renderer component to use in the UI.
- Examples:
"markdown","dashboard"
audience (optional)
- Type:
string - Default:
"technical" - Description: Target audience for the document. Used for access control and filtering.
- Examples:
"technical","business","executive","contributor","compliance" - Validation: If
audiencesarray exists at top level, this value must be in that array.
category (required)
- Type:
string - Constraints: 1-100 characters
- Description: Document category. Must match the
namefield of aCategoryobject in the top-levelcategoriesarray. - Examples:
"Architecture","Compliance","Session Logs"
keywords (optional)
- Type:
arrayofstring - Constraints: Unique items, each 1-100 characters
- Description: Keywords for search indexing. Extracted from frontmatter or auto-generated.
- Examples:
["architecture", "system-design", "microservices"]
summary (optional)
- Type:
string - Constraints: Max 1000 characters
- Description: Brief summary or description of the document. Extracted from frontmatter
summaryfield. - Example:
"High-level system architecture and component design for the QMS platform."
author (optional)
- Type:
string - Constraints: Max 200 characters
- Description: Document author. Extracted from frontmatter
authorfield. - Examples:
"Hal Casteel","Claude (Opus 4.6)"
status (optional)
- Type:
string(enum) - Allowed Values:
draft,active,archived,deprecated - Default:
"active" - Description: Document lifecycle status. Used for filtering and UI display.
- Examples:
"active","draft"
body_text (optional)
- Type:
string - Constraints: Max 1,000,000 characters
- Description: Searchable plain-text version of the document content with markdown formatting stripped. Used for full-text search indexing.
- Example:
"System Design Document The CODITECT platform uses..." - Note: This field can be omitted for dashboards or when full-text search is not required.
Validation Rules
Schema Validation
Level 1: JSON Schema Structural Validation
All fields must conform to the JSON Schema definition above:
- Required fields present
- Types match (string, integer, array, object)
- String lengths within bounds
- Enum values valid
- Regex patterns match (version, document ID)
- URI/date-time formats valid
Tool: Ajv JSON Schema Validator (Draft 2020-12 support)
Referential Integrity Validation
Level 2: Cross-Field Consistency Checks
These rules enforce data consistency across the manifest:
Rule 1: Total Documents Count
total_documents === documents.length
Error: "total_documents (133) does not match actual document count (134)"
Rule 2: Category Coverage
For each category in categories:
At least one document must have category === category.name
Error: "Category 'Testing' has no matching documents"
Rule 3: Category Count Accuracy
For each category in categories:
category.count === count of documents where document.category === category.name
Error: "Category 'Architecture' reports count=6 but actual count is 7"
Rule 4: Category Type Breakdown
For each category in categories:
For each (type, count) in category.types:
count === count of documents where document.category === category.name AND document.type === type
Error: "Category 'Compliance' reports 4 dashboards but actual count is 3"
Rule 5: Type Sum Equals Category Count
For each category in categories:
sum(category.types.values()) === category.count
Error: "Category 'Executive' type counts (3+2=5) do not sum to category.count (6)"
Rule 6: Unique Document IDs
documents.map(d => d.id) has no duplicates
Error: "Duplicate document ID: 'docs-architecture-system-design'"
Rule 7: Audience Enumeration
If audiences array is defined:
For each document in documents:
document.audience must be in audiences
Error: "Document 'abc-123' has audience 'guest' which is not in the defined audiences list"
Rule 8: Document Category Membership
For each document in documents:
There must exist a category in categories where category.name === document.category
Error: "Document 'xyz-789' has category 'Unknown' which does not exist in categories array"
Content Validation (Optional)
Level 3: File Existence and Content Checks
These checks verify that document metadata matches actual files:
Rule 9: File Existence
For each document in documents:
File exists at document.path
Error: "Document path 'docs/missing.md' does not exist on filesystem"
Rule 10: Frontmatter Consistency
For each markdown document in documents:
If file has frontmatter:
frontmatter.title === document.title (if present)
frontmatter.audience === document.audience (if present)
frontmatter.keywords ⊆ document.keywords (if present)
Warning: "Document title 'Foo' does not match frontmatter title 'Bar' at docs/foo.md"
CLI Validation Command
Command Specification
Script Location: scripts/validate-publish-manifest.js
Package.json Entry:
{
"scripts": {
"validate-manifest": "node scripts/validate-publish-manifest.js"
}
}
Usage:
npm run validate-manifest
Options:
npm run validate-manifest -- --strict # Enable all optional checks (file existence, frontmatter)
npm run validate-manifest -- --verbose # Detailed validation output
npm run validate-manifest -- --file custom.json # Validate a different file
npm run validate-manifest -- --schema-only # Only JSON Schema validation, skip referential integrity
Implementation
Dependencies
{
"devDependencies": {
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1"
}
}
Script Structure
#!/usr/bin/env node
/**
* A.4.1: Validate publish.json against schema and referential integrity rules.
*
* Usage: node scripts/validate-publish-manifest.js [--strict] [--verbose] [--file <path>]
*/
import { readFileSync } from "fs";
import Ajv from "ajv";
import addFormats from "ajv-formats";
const args = process.argv.slice(2);
const STRICT = args.includes("--strict");
const VERBOSE = args.includes("--verbose");
const fileArg = args.indexOf("--file");
const MANIFEST_PATH = fileArg > -1 ? args[fileArg + 1] : "public/publish.json";
const SCHEMA_ONLY = args.includes("--schema-only");
const errors = [];
const warnings = [];
function error(msg) {
errors.push(msg);
console.error(`❌ ERROR: ${msg}`);
}
function warn(msg) {
warnings.push(msg);
if (VERBOSE) console.warn(`⚠️ WARNING: ${msg}`);
}
function info(msg) {
if (VERBOSE) console.log(`ℹ️ INFO: ${msg}`);
}
// Load manifest
let manifest;
try {
manifest = JSON.parse(readFileSync(MANIFEST_PATH, "utf-8"));
info(`Loaded manifest from ${MANIFEST_PATH}`);
} catch (err) {
console.error(`❌ FATAL: Failed to load manifest from ${MANIFEST_PATH}: ${err.message}`);
process.exit(1);
}
// Load schema
let schema;
try {
schema = JSON.parse(readFileSync("config/schemas/publish-manifest.schema.json", "utf-8"));
info("Loaded schema from config/schemas/publish-manifest.schema.json");
} catch (err) {
console.error(`❌ FATAL: Failed to load schema: ${err.message}`);
process.exit(1);
}
// JSON Schema Validation
const ajv = new Ajv({ allErrors: true, strict: false });
addFormats(ajv);
const validate = ajv.compile(schema);
const valid = validate(manifest);
if (!valid) {
error("JSON Schema validation failed:");
for (const err of validate.errors) {
const path = err.instancePath || "/";
error(` ${path}: ${err.message} (${JSON.stringify(err.params)})`);
}
}
if (SCHEMA_ONLY) {
console.log(errors.length === 0 ? "✅ Schema validation passed" : `❌ ${errors.length} schema errors`);
process.exit(errors.length > 0 ? 1 : 0);
}
// Referential Integrity Validation
info("Running referential integrity checks...");
// Rule 1: Total documents count
if (manifest.total_documents !== manifest.documents.length) {
error(`total_documents (${manifest.total_documents}) does not match actual document count (${manifest.documents.length})`);
}
// Rule 2 & 3 & 4 & 5: Category integrity
const categoryMap = new Map();
for (const doc of manifest.documents) {
const key = doc.category;
if (!categoryMap.has(key)) {
categoryMap.set(key, { total: 0, types: {} });
}
categoryMap.get(key).total++;
categoryMap.get(key).types[doc.type] = (categoryMap.get(key).types[doc.type] || 0) + 1;
}
for (const cat of manifest.categories) {
const actual = categoryMap.get(cat.name);
if (!actual) {
error(`Category '${cat.name}' has no matching documents`);
continue;
}
if (cat.count !== actual.total) {
error(`Category '${cat.name}' reports count=${cat.count} but actual count is ${actual.total}`);
}
const typeSum = Object.values(cat.types).reduce((sum, n) => sum + n, 0);
if (typeSum !== cat.count) {
error(`Category '${cat.name}' type counts (${typeSum}) do not sum to category.count (${cat.count})`);
}
for (const [type, count] of Object.entries(cat.types)) {
const actualCount = actual.types[type] || 0;
if (count !== actualCount) {
error(`Category '${cat.name}' reports ${count} ${type} docs but actual count is ${actualCount}`);
}
}
}
// Rule 6: Unique document IDs
const idSet = new Set();
for (const doc of manifest.documents) {
if (idSet.has(doc.id)) {
error(`Duplicate document ID: '${doc.id}'`);
}
idSet.add(doc.id);
}
// Rule 7: Audience enumeration
if (manifest.audiences && manifest.audiences.length > 0) {
const audienceSet = new Set(manifest.audiences);
for (const doc of manifest.documents) {
if (doc.audience && !audienceSet.has(doc.audience)) {
error(`Document '${doc.id}' has audience '${doc.audience}' which is not in the defined audiences list`);
}
}
}
// Rule 8: Document category membership
const categoryNames = new Set(manifest.categories.map(c => c.name));
for (const doc of manifest.documents) {
if (!categoryNames.has(doc.category)) {
error(`Document '${doc.id}' has category '${doc.category}' which does not exist in categories array`);
}
}
// Optional: File existence check (--strict)
if (STRICT) {
info("Running strict file existence checks...");
const { statSync } = await import("fs");
for (const doc of manifest.documents) {
try {
statSync(doc.path);
} catch {
warn(`Document path '${doc.path}' does not exist on filesystem (ID: ${doc.id})`);
}
}
}
// Report
console.log("\n" + "=".repeat(60));
if (errors.length === 0 && warnings.length === 0) {
console.log("✅ Validation passed: 0 errors, 0 warnings");
process.exit(0);
} else if (errors.length === 0) {
console.log(`⚠️ Validation passed with warnings: ${warnings.length} warnings`);
process.exit(0);
} else {
console.log(`❌ Validation failed: ${errors.length} errors, ${warnings.length} warnings`);
process.exit(1);
}
Exit Codes
| Code | Meaning |
|---|---|
0 | Validation passed (0 errors) |
1 | Validation failed (1+ errors) or fatal error (file not found, invalid JSON) |
CI Integration
Add to .github/workflows/ci.yml or similar:
- name: Validate Publish Manifest
run: npm run validate-manifest
Error Handling
Error Categories
| Category | Severity | Exit Code | Description |
|---|---|---|---|
| Fatal | Critical | 1 | Manifest file missing, invalid JSON, schema file missing |
| Schema Error | High | 1 | JSON Schema validation failure (type mismatch, required field missing, format violation) |
| Integrity Error | High | 1 | Referential integrity violation (count mismatch, orphan category, duplicate ID) |
| File Warning | Low | 0 | File existence check failed (--strict mode only) |
| Frontmatter Warning | Low | 0 | Frontmatter mismatch (--strict mode only) |
Error Message Format
Structure:
❌ ERROR: <Error Type>: <Detailed Message>
Location: <manifest path or field>
Expected: <expected value>
Actual: <actual value>
Examples:
❌ ERROR: total_documents (133) does not match actual document count (134)
Location: /total_documents
Expected: 134
Actual: 133
❌ ERROR: Duplicate document ID: 'docs-architecture-system-design'
Location: /documents[47].id
First occurrence: /documents[23].id
❌ ERROR: Document category 'Unknown' does not exist in categories array
Location: /documents[89].category
Document ID: internal-analysis-foo-bar
Available categories: Architecture, Compliance, Executive, ...
Warning Message Format
⚠️ WARNING: <Warning Type>: <Detailed Message>
Location: <manifest path or file path>
Recommendation: <suggested fix>
Example:
⚠️ WARNING: Document path 'docs/draft/wip.md' does not exist on filesystem
Location: /documents[102].path
Document ID: docs-draft-wip
Recommendation: Remove document entry or ensure file exists before build
Schema Versioning
Versioning Strategy
The publish.json schema uses semantic versioning (SemVer 2.0.0):
- Major version (
X.0.0): Breaking changes (field removal, type change, new required field) - Minor version (
0.X.0): Backward-compatible additions (new optional field, new enum value) - Patch version (
0.0.X): Bug fixes, documentation updates, validation rule clarifications
Current Version: 1.0.0
Schema File Naming
Schema files are versioned and stored with the version in the filename:
config/schemas/publish-manifest-v1.0.0.schema.json
config/schemas/publish-manifest-v1.1.0.schema.json
config/schemas/publish-manifest-v2.0.0.schema.json
A symlink publish-manifest.schema.json always points to the latest version:
cd config/schemas
ln -sf publish-manifest-v1.0.0.schema.json publish-manifest.schema.json
Manifest Version Field
The top-level version field in publish.json indicates the schema version (and optionally project version):
{
"version": "1.0.0",
"project_name": "CODITECT Bioscience QMS Platform",
...
}
Interpretation:
- Validator loads the schema matching the manifest version
- If schema version not found, validator falls back to latest schema and warns
Breaking Change Policy
Before introducing a breaking change:
- Increment major version (e.g.,
1.x.x→2.0.0) - Create new schema file:
publish-manifest-v2.0.0.schema.json - Update generation script to output new format
- Provide migration tool (see Migration Guide)
- Maintain backward compatibility for 1 release cycle (validator supports both v1 and v2 schemas)
Communication:
- Breaking changes announced in release notes
- Migration guide published before release
- Deprecation warnings added to v1 schema for removed/changed fields
Build Pipeline Integration
Pre-Build Validation
Vite Build Script Integration:
Update package.json:
{
"scripts": {
"prebuild": "npm run generate-manifest && npm run validate-manifest",
"build": "vite build"
}
}
Execution Order:
npm run buildinvokedprebuildhook runs automatically:generate-manifestcreates/updatespublic/publish.jsonvalidate-manifestvalidates the generated manifest- If validation fails (exit code 1), build aborts
vite buildruns only if prebuild succeeds
CI/CD Pipeline
GitHub Actions Example:
name: Build and Deploy
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Dependencies
run: npm ci
- name: Generate Publish Manifest
run: npm run generate-manifest
- name: Validate Publish Manifest
run: npm run validate-manifest
- name: Build
run: npm run build
- name: Deploy
if: github.ref == 'refs/heads/main'
run: npm run deploy
Benefits:
- Manifest validation runs before build (fast failure)
- PR checks catch manifest errors before merge
- Production builds guaranteed to have valid manifests
Local Development
Pre-Commit Hook (Optional):
Install husky for Git hooks:
npm install --save-dev husky
npx husky init
Add .husky/pre-commit:
#!/bin/sh
npm run validate-manifest
Effect: Manifest validation runs before every commit. Blocks commits if validation fails.
Examples
Example 1: Valid Minimal Manifest
{
"project_name": "Example Project",
"version": "1.0.0",
"generated_at": "2026-02-16T12:00:00.000Z",
"total_documents": 2,
"categories": [
{
"name": "Docs",
"count": 2,
"types": {
"markdown": 2
}
}
],
"documents": [
{
"id": "readme",
"title": "README",
"path": "README.md",
"type": "markdown",
"category": "Docs"
},
{
"id": "guide",
"title": "User Guide",
"path": "docs/guide.md",
"type": "markdown",
"category": "Docs",
"audience": "technical",
"keywords": ["user-guide", "tutorial"],
"summary": "Comprehensive user guide for new users.",
"author": "Engineering Team",
"status": "active"
}
]
}
Validation Result: ✅ Pass
Example 2: Valid Full-Featured Manifest
{
"project_name": "CODITECT Bioscience QMS Platform",
"version": "1.0.0",
"generated_at": "2026-02-16T04:29:25.849Z",
"base_url": "https://qms.coditect.ai",
"auth_mode": "role-based",
"total_documents": 3,
"audiences": ["technical", "business", "executive"],
"categories": [
{
"name": "Architecture",
"count": 1,
"types": {"markdown": 1}
},
{
"name": "Business",
"count": 2,
"types": {"dashboard": 2}
}
],
"documents": [
{
"id": "docs-architecture-system-design",
"title": "System Design Document",
"path": "docs/architecture/system-design.md",
"type": "markdown",
"audience": "technical",
"category": "Architecture",
"keywords": ["architecture", "system-design", "microservices"],
"summary": "High-level system architecture and component design.",
"author": "Hal Casteel",
"status": "active",
"body_text": "System Design Document The CODITECT platform..."
},
{
"id": "dashboards-business-revenue-forecast",
"title": "Revenue Forecast Dashboard",
"path": "dashboards/business/revenue-forecast.jsx",
"type": "dashboard",
"audience": "business",
"category": "Business",
"keywords": ["revenue", "forecast", "metrics"],
"summary": "Interactive revenue forecasting dashboard.",
"status": "active"
},
{
"id": "dashboards-business-burn-rate",
"title": "Burn Rate Tracker",
"path": "dashboards/business/burn-rate.jsx",
"type": "dashboard",
"audience": "executive",
"category": "Business",
"keywords": ["burn-rate", "financial", "runway"],
"summary": "Monthly burn rate and runway projection.",
"status": "active"
}
]
}
Validation Result: ✅ Pass
Example 3: Invalid Manifest (Count Mismatch)
{
"project_name": "Example",
"version": "1.0.0",
"generated_at": "2026-02-16T12:00:00.000Z",
"total_documents": 5,
"categories": [
{
"name": "Docs",
"count": 3,
"types": {"markdown": 3}
}
],
"documents": [
{
"id": "doc1",
"title": "Doc 1",
"path": "doc1.md",
"type": "markdown",
"category": "Docs"
},
{
"id": "doc2",
"title": "Doc 2",
"path": "doc2.md",
"type": "markdown",
"category": "Docs"
}
]
}
Validation Errors:
❌ ERROR: total_documents (5) does not match actual document count (2)
Location: /total_documents
Expected: 2
Actual: 5
❌ ERROR: Category 'Docs' reports count=3 but actual count is 2
Location: /categories[0].count
Expected: 2
Actual: 3
Example 4: Invalid Manifest (Referential Integrity)
{
"project_name": "Example",
"version": "1.0.0",
"generated_at": "2026-02-16T12:00:00.000Z",
"total_documents": 2,
"audiences": ["technical", "business"],
"categories": [
{
"name": "Docs",
"count": 2,
"types": {"markdown": 2}
}
],
"documents": [
{
"id": "doc1",
"title": "Doc 1",
"path": "doc1.md",
"type": "markdown",
"category": "Docs",
"audience": "technical"
},
{
"id": "doc2",
"title": "Doc 2",
"path": "doc2.md",
"type": "markdown",
"category": "Unknown",
"audience": "guest"
}
]
}
Validation Errors:
❌ ERROR: Document 'doc2' has audience 'guest' which is not in the defined audiences list
Location: /documents[1].audience
Document ID: doc2
Available audiences: technical, business
❌ ERROR: Document 'doc2' has category 'Unknown' which does not exist in categories array
Location: /documents[1].category
Document ID: doc2
Available categories: Docs
Migration Guide
Schema Version Upgrade Workflow
When a new schema version is released (e.g., v1.0.0 → v2.0.0), follow this process to migrate existing manifests:
Step 1: Identify Schema Changes
Review Release Notes:
- Read the schema changelog at
config/schemas/CHANGELOG.md - Identify breaking changes (removed fields, type changes, new required fields)
- Identify new optional fields and features
Example Breaking Change (v1 → v2):
v2.0.0 Breaking Changes:
- Removed field:
body_text(migrated to separate search index)- New required field:
schema_version(must be"2.0.0")- Renamed field:
audiences→access_roles- Type change:
generated_atnow requires timezone (+00:00orZ)
Step 2: Run Migration Tool
Install Migration Script:
npm install --save-dev publish-manifest-migrator
Run Migration:
npx publish-manifest-migrator \
--from v1.0.0 \
--to v2.0.0 \
--input public/publish.json \
--output public/publish-v2.json \
--backup
Options:
--from: Source schema version--to: Target schema version--input: Input manifest file (default:public/publish.json)--output: Output file (default:public/publish-migrated.json)--backup: Create backup atpublic/publish.json.bakbefore migration--dry-run: Show migration plan without writing files--verbose: Detailed migration logging
Step 3: Manual Review (Breaking Changes Only)
Review Automated Changes:
Compare old and new manifests:
diff public/publish.json public/publish-v2.json
Verify:
- All required fields populated
- Renamed fields correctly mapped
- Type conversions valid
- No data loss on removed fields (check migration warnings)
Step 4: Validate Migrated Manifest
npm run validate-manifest -- --file public/publish-v2.json
If validation passes:
✅ Validation passed: 0 errors, 0 warnings
If validation fails: Review errors and adjust migration manually.
Step 5: Update Generation Script
Update scripts/generate-publish-manifest.js:
// Old (v1):
const manifest = {
project_name: "...",
version: "1.0.0",
audiences: ["technical", "business"],
documents: [...],
};
// New (v2):
const manifest = {
schema_version: "2.0.0", // NEW REQUIRED FIELD
project_name: "...",
version: "2.0.0",
access_roles: ["technical", "business"], // RENAMED
documents: documents.map(d => ({
...d,
// body_text removed — now in separate search index
})),
};
Test Generation:
npm run generate-manifest
npm run validate-manifest
Step 6: Deploy
Replace Old Manifest:
mv public/publish-v2.json public/publish.json
Commit and Deploy:
git add public/publish.json scripts/generate-publish-manifest.js
git commit -m "chore: Migrate publish manifest to schema v2.0.0"
npm run build
npm run deploy
Migration Script Example
scripts/migrate-publish-manifest.js:
#!/usr/bin/env node
import { readFileSync, writeFileSync, copyFileSync } from "fs";
const args = process.argv.slice(2);
const inputFile = args.find(a => a.startsWith("--input="))?.split("=")[1] || "public/publish.json";
const outputFile = args.find(a => a.startsWith("--output="))?.split("=")[1] || "public/publish-migrated.json";
const backup = args.includes("--backup");
if (backup) {
copyFileSync(inputFile, `${inputFile}.bak`);
console.log(`✅ Backup created: ${inputFile}.bak`);
}
const v1 = JSON.parse(readFileSync(inputFile, "utf-8"));
// Migration logic: v1 → v2
const v2 = {
schema_version: "2.0.0", // NEW REQUIRED FIELD
project_name: v1.project_name,
version: "2.0.0",
generated_at: ensureTimezone(v1.generated_at), // TYPE CHANGE: ensure timezone
base_url: v1.base_url,
auth_mode: v1.auth_mode,
total_documents: v1.total_documents,
access_roles: v1.audiences || [], // RENAMED: audiences → access_roles
categories: v1.categories,
documents: v1.documents.map(doc => ({
id: doc.id,
title: doc.title,
path: doc.path,
type: doc.type,
audience: doc.audience,
category: doc.category,
keywords: doc.keywords,
summary: doc.summary,
author: doc.author,
status: doc.status,
// body_text REMOVED — migrated to separate search index
})),
};
writeFileSync(outputFile, JSON.stringify(v2, null, 2));
console.log(`✅ Migrated manifest written to: ${outputFile}`);
function ensureTimezone(dateStr) {
// v2 requires timezone — add 'Z' if missing
return dateStr.endsWith("Z") ? dateStr : `${dateStr}Z`;
}
Rollback Procedure
If migration fails or causes issues:
# Restore backup
mv public/publish.json.bak public/publish.json
# Revert generation script
git checkout HEAD -- scripts/generate-publish-manifest.js
# Rebuild
npm run build
Appendices
Appendix A: JSON Schema Draft 2020-12 References
Specification: https://json-schema.org/draft/2020-12/json-schema-core.html
Key Features Used:
$schema: Declares schema dialect$id: Unique schema identifier (URI)$ref: References definitions (#/$defs/Category)$defs: Schema definitions (reusable objects)type: Validates JSON type (object, array, string, integer, boolean, null)required: Array of required property namesproperties: Object property schemasadditionalProperties: Disallow unlisted properties (false)enum: Restrict to specific valuespattern: Regex validation for stringsformat: Semantic validation (date-time, uri, email, etc.)minLength,maxLength: String length constraintsminimum,maximum: Numeric range constraintsminItems,maxItems: Array length constraintsuniqueItems: Enforce unique array elementsminProperties,maxProperties: Object property count constraintspatternProperties: Validate dynamic property names
Appendix B: Example Category Mappings
From scripts/generate-publish-manifest.js:
const CATEGORY_MAP = {
executive: "Executive",
market: "Market",
architecture: "Architecture",
compliance: "Compliance",
operations: "Operations",
product: "Product",
reference: "Reference",
agents: "Agents",
"state-machine": "State Machine",
research: "Research",
system: "System",
business: "Business",
planning: "Planning",
tracks: "Project Tracking",
plans: "Project Tracking",
analysis: "Analysis",
"bio-qms-completeness": "Analysis",
"bio-qms-research-coverage": "Analysis",
prompts: "Prompts",
config: "Configuration",
};
Usage: Directory name (e.g., docs/architecture/) maps to category name (e.g., "Architecture").
Appendix C: Document Type Renderers
| Type | File Extension | Renderer Component | Description |
|---|---|---|---|
markdown | .md | MarkdownViewer.jsx | Unified/rehype pipeline with GFM, math, syntax highlighting |
dashboard | .jsx | DashboardRenderer.jsx | Dynamic React component import |
pdf | .pdf | PDFViewer.jsx | PDF.js-based viewer |
html | .html | HTMLViewer.jsx | Sandboxed iframe |
docx | .docx | DocxViewer.jsx | Mammoth.js conversion to HTML |
Appendix D: Full-Text Search Integration
The body_text field contains markdown-stripped plain text for search indexing.
Search Engine: MiniSearch (https://github.com/lucaong/minisearch)
Indexing Configuration:
import MiniSearch from 'minisearch';
const searchIndex = new MiniSearch({
fields: ['title', 'body_text', 'keywords', 'summary', 'author'],
storeFields: ['id', 'title', 'path', 'category', 'audience'],
searchOptions: {
boost: { title: 3, keywords: 2, summary: 1.5 },
fuzzy: 0.2,
prefix: true,
}
});
// Load from publish.json
const manifest = await fetch('/publish.json').then(r => r.json());
searchIndex.addAll(manifest.documents);
// Search
const results = searchIndex.search('system design architecture');
Ranking Factors:
- Title match (3x boost)
- Keyword match (2x boost)
- Summary match (1.5x boost)
- Body text match (1x baseline)
- Fuzzy matching (Levenshtein distance ≤ 2)
- Prefix matching (autocomplete)
Appendix E: Access Control Integration
When auth_mode is "role-based", the document.audience field maps to user roles:
Example User Session:
{
"user_id": "u123",
"email": "engineer@example.com",
"roles": ["technical", "contributor"]
}
Access Control Logic:
function canAccessDocument(user, document) {
if (!document.audience) return true; // No restriction
return user.roles.includes(document.audience);
}
// Filter documents by user access
const accessibleDocs = manifest.documents.filter(doc => canAccessDocument(user, doc));
Role Hierarchy (Optional):
const ROLE_HIERARCHY = {
executive: ["executive", "business", "technical", "contributor"],
business: ["business", "technical", "contributor"],
technical: ["technical", "contributor"],
contributor: ["contributor"],
compliance: ["compliance", "contributor"],
};
function getUserPermittedAudiences(user) {
const permitted = new Set();
for (const role of user.roles) {
for (const audience of ROLE_HIERARCHY[role] || []) {
permitted.add(audience);
}
}
return Array.from(permitted);
}
Appendix F: Performance Considerations
Manifest Size:
- Current production manifest: ~1.7 MB (133 documents with full
body_text) - Gzipped size: ~200 KB
- Load time (CDN): ~100-200ms on 3G, ~20-50ms on 4G
Optimization Strategies:
- Lazy Load Body Text: Omit
body_textfrom main manifest, load on-demand for search - Incremental Indexing: Split search index by category, load on category navigation
- CDN Caching: Set
Cache-Control: public, max-age=3600(1 hour) - Brotli Compression: Enable Brotli for better compression (~30% smaller than gzip)
- Pagination: If document count exceeds 1000, implement paginated manifest API
Large-Scale Manifest (1000+ documents):
Consider migrating to a database-backed API:
GET /api/documents?category=Architecture&limit=50&offset=0
GET /api/search?q=system+design&limit=20
Appendix G: Related Standards and Specifications
- JSON Schema Draft 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html
- Semantic Versioning 2.0.0: https://semver.org/
- ISO 8601 Date and Time Format: https://www.iso.org/iso-8601-date-and-time-format.html
- RFC 3986 (URI Generic Syntax): https://tools.ietf.org/html/rfc3986
- YAML 1.2 Frontmatter: https://yaml.org/spec/1.2/spec.html
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0.0 | 2026-02-16 | Claude (Sonnet 4.5) | Initial specification for BIO-QMS task A.4.1 |
License
Copyright © 2026 AZ1.AI INC
This document is proprietary to CODITECT and intended for internal use only. Unauthorized distribution is prohibited.
End of Document