Skip to main content

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 implementation
  • public/publish.json: Production manifest file (133+ documents)

Table of Contents

  1. Schema Definition
  2. Field Specifications
  3. Validation Rules
  4. CLI Validation Command
  5. Error Handling
  6. Schema Versioning
  7. Build Pipeline Integration
  8. Examples
  9. Migration Guide
  10. 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 equal documents.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 documents array.

categories (required)

  • Type: array of Category objects
  • 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: array of string
  • Constraints: Unique items, non-empty strings
  • Description: Enumeration of valid audience types for the project. If present, all document.audience values must be in this list.
  • Examples: ["technical", "business", "executive", "contributor", "compliance"]
  • Validation: Must be unique, each item >= 1 character.

documents (required)

  • Type: array of Document objects
  • 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 categories array
    • If audiences is defined, all document.audience values 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 category field.
  • 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 title field 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 audiences array 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 name field of a Category object in the top-level categories array.
  • Examples: "Architecture", "Compliance", "Session Logs"

keywords (optional)

  • Type: array of string
  • 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 summary field.
  • 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 author field.
  • 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

CodeMeaning
0Validation passed (0 errors)
1Validation 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

CategorySeverityExit CodeDescription
FatalCritical1Manifest file missing, invalid JSON, schema file missing
Schema ErrorHigh1JSON Schema validation failure (type mismatch, required field missing, format violation)
Integrity ErrorHigh1Referential integrity violation (count mismatch, orphan category, duplicate ID)
File WarningLow0File existence check failed (--strict mode only)
Frontmatter WarningLow0Frontmatter 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:

  1. Increment major version (e.g., 1.x.x2.0.0)
  2. Create new schema file: publish-manifest-v2.0.0.schema.json
  3. Update generation script to output new format
  4. Provide migration tool (see Migration Guide)
  5. 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:

  1. npm run build invoked
  2. prebuild hook runs automatically:
    • generate-manifest creates/updates public/publish.json
    • validate-manifest validates the generated manifest
    • If validation fails (exit code 1), build aborts
  3. vite build runs 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


{
"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.0v2.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: audiencesaccess_roles
  • Type change: generated_at now requires timezone (+00:00 or Z)

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 at public/publish.json.bak before 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 names
  • properties: Object property schemas
  • additionalProperties: Disallow unlisted properties (false)
  • enum: Restrict to specific values
  • pattern: Regex validation for strings
  • format: Semantic validation (date-time, uri, email, etc.)
  • minLength, maxLength: String length constraints
  • minimum, maximum: Numeric range constraints
  • minItems, maxItems: Array length constraints
  • uniqueItems: Enforce unique array elements
  • minProperties, maxProperties: Object property count constraints
  • patternProperties: 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

TypeFile ExtensionRenderer ComponentDescription
markdown.mdMarkdownViewer.jsxUnified/rehype pipeline with GFM, math, syntax highlighting
dashboard.jsxDashboardRenderer.jsxDynamic React component import
pdf.pdfPDFViewer.jsxPDF.js-based viewer
html.htmlHTMLViewer.jsxSandboxed iframe
docx.docxDocxViewer.jsxMammoth.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:

  1. Title match (3x boost)
  2. Keyword match (2x boost)
  3. Summary match (1.5x boost)
  4. Body text match (1x baseline)
  5. Fuzzy matching (Levenshtein distance ≤ 2)
  6. 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:

  1. Lazy Load Body Text: Omit body_text from main manifest, load on-demand for search
  2. Incremental Indexing: Split search index by category, load on category navigation
  3. CDN Caching: Set Cache-Control: public, max-age=3600 (1 hour)
  4. Brotli Compression: Enable Brotli for better compression (~30% smaller than gzip)
  5. 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


Document History

VersionDateAuthorChanges
1.0.02026-02-16Claude (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