#!/usr/bin/env node
/**
 * print-schema.js — single canonical way for skills / agents to learn the
 * shape of SDD artifacts.
 *
 * Skills and agents run this to get the current schema instead of embedding
 * hand-authored templates. The data comes directly from sdd-utils.js's exported
 * rule constants, which are also what the validators consume — so there is one
 * source of truth and the printed schema cannot drift from the validator.
 *
 * Usage:
 *   node scripts/lib/print-schema.js design               → terminal-friendly text (default)
 *   node scripts/lib/print-schema.js design --markdown    → committed-markdown form
 *                                                          (generated view; wraps regex
 *                                                          in inline code, adds do-not-edit header)
 *   node scripts/lib/print-schema.js design --json        → JSON machine form
 *   node scripts/lib/print-schema.js spec [--markdown|--json]
 *
 * Examples:
 *   The decision-log and pending-conflict renderers emit valid + invalid
 *   examples per fr-sd-013-ac3 and fr-sd-012-ac3. Examples are renderer
 *   prose (lines.push string literals); structured field data still comes
 *   from the rule constants. The design and spec renderers do not emit
 *   examples — their fr-sd-002-ac2/fr-sd-003-ac3/fr-sd-007-ac1 examples
 *   live in test fixtures (tests/scripts/sdd-utils.test.js).
 */

const {
  DESIGN_DOC_RULES,
  SPEC_HEADER_RULES,
  DECISION_LOG_RULES,
  PENDING_CONFLICT_RULES,
  VISION_RULES,
  DECISION_LEDGER_RULES,
} = require('./sdd-utils');

// Markdown mode wraps regex values in inline code so renderers (GitHub, Obsidian)
// don't misinterpret characters like `[`, `]`, `*`, `\` as markdown syntax.
// Plain mode (terminal) skips the backticks for readability in narrow terminals.
function fmtRegex(source, markdown) {
  return markdown ? `\`${source}\`` : source;
}
function fmtCode(s, markdown) {
  return markdown ? `\`${s}\`` : s;
}

const GENERATED_HEADER = [
  '<!--',
  '  AUTO-GENERATED from scripts/lib/sdd-utils.js (DESIGN_DOC_RULES, SPEC_HEADER_RULES)',
  '  and scripts/lib/sdd-rules.js (DECISION_LOG_RULES, PENDING_CONFLICT_RULES,',
  '  VISION_RULES, DECISION_LEDGER_RULES).',
  '  Do NOT edit this file by hand. To change the schema, edit the source constant in',
  '  the corresponding module and regenerate:',
  '',
  '    node scripts/lib/print-schema.js design            --markdown > scripts/lib/sdd-schemas/design.md',
  '    node scripts/lib/print-schema.js spec              --markdown > scripts/lib/sdd-schemas/spec.md',
  '    node scripts/lib/print-schema.js decision-log      --markdown > scripts/lib/sdd-schemas/decision-log.md',
  '    node scripts/lib/print-schema.js pending-conflict  --markdown > scripts/lib/sdd-schemas/pending-conflict.md',
  '    node scripts/lib/print-schema.js vision            --markdown > scripts/lib/sdd-schemas/vision.md',
  '    node scripts/lib/print-schema.js decision-ledger   --markdown > scripts/lib/sdd-schemas/decision-ledger.md',
  '',
  '  tests/scripts/sdd-schemas-fresh.test.js asserts this file stays in sync with the',
  '  source — a stale copy will fail CI.',
  '-->',
  '',
];

function renderDesign(opts = {}) {
  const md = !!opts.markdown;
  const r = DESIGN_DOC_RULES;
  const lines = [];
  if (md) lines.push(...GENERATED_HEADER);
  lines.push('# Design Doc Schema');
  lines.push('');
  lines.push(`Canonical path: ${fmtCode(r.canonical_path, md)}`);
  lines.push(`Path regex: ${fmtRegex(r.path_regex.source, md)}`);
  lines.push(`Substantive content: at least ${r.substantive_min_chars} non-heading characters.`);
  lines.push('');
  lines.push('## Filesystem-determined shape (one behavior, no modes)');
  lines.push('');
  lines.push('The refiner checks whether `specs/<spec-id>/spec.xml` exists. One of two');
  lines.push('shapes applies — no mode selector, no branching skill; just conditional fields.');
  lines.push('');
  lines.push('### When NO prior spec exists (`hasPriorSpec = false`)');
  lines.push('');
  lines.push(r.initial.description);
  lines.push('');
  lines.push('Required prose elements (refiner extracts from prose — no mandatory headings):');
  lines.push('');
  for (const e of r.initial.required_prose_elements) lines.push(`- ${e}`);
  lines.push('');
  lines.push('### When a prior spec exists (`hasPriorSpec = true`)');
  lines.push('');
  lines.push(r.iteration.description);
  lines.push('');
  lines.push('Required section headings (matched via `section_regex` — see below):');
  lines.push('');
  for (const s of r.iteration.required_sections) lines.push(`- ${s}`);
  lines.push('');
  lines.push('Recommended section headings:');
  lines.push('');
  for (const s of r.iteration.recommended_sections) lines.push(`- ${s}`);
  lines.push('');
  lines.push('Forbidden section keywords (must NOT appear as top-level headings — the');
  lines.push('refiner derives the delta itself from narrative, never from a pre-authored list):');
  lines.push('');
  for (const s of r.iteration.forbidden_section_keywords) lines.push(`- ${s}`);
  lines.push('');
  lines.push('## Section heading regexes');
  lines.push('');
  lines.push('Headings are detected via these patterns. They tolerate trailing suffix text');
  lines.push('(e.g., `## Context (from spec v1)`, `## Context — 2026-04-19`) while rejecting');
  lines.push('prefix words like `## Contextual Factors`.');
  lines.push('');
  for (const [name, re] of Object.entries(r.section_regex)) {
    lines.push(`- ${name}: ${fmtRegex(re.source, md)}`);
  }
  lines.push('');
  lines.push('## Enforcement authority');
  lines.push('');
  lines.push('`scripts/lib/sdd-utils.js` — `validateDesignDoc(parsed)` is the sole enforcement.');
  lines.push('If this printed schema disagrees with the validator, the validator wins and');
  lines.push('this tool is wrong (file a bug).');
  return lines.join('\n');
}

function renderSpec(opts = {}) {
  const md = !!opts.markdown;
  const r = SPEC_HEADER_RULES;
  const lines = [];
  if (md) lines.push(...GENERATED_HEADER);
  lines.push('# Spec Identity Header Schema');
  lines.push('');
  lines.push(`Canonical path: ${fmtCode(r.canonical_path, md)}`);
  lines.push(`\`design_iteration\` regex: ${fmtRegex(r.design_iteration_regex.source, md)}`);
  lines.push(`\`supersedes\` regex: ${fmtRegex(r.supersedes_regex.source, md)}`);
  lines.push('');
  lines.push('## Always-required fields (missing any → ERROR)');
  lines.push('');
  const fmtType = (f) => {
    if (Array.isArray(f.allowed)) {
      return `${f.type} (allowed: ${JSON.stringify(f.allowed)})`;
    }
    return f.type;
  };
  if (md) {
    lines.push('| Field | Type |');
    lines.push('|---|---|');
    for (const f of r.required_fields) {
      lines.push(`| \`${f.field}\` | ${fmtType(f)} |`);
    }
  } else {
    for (const f of r.required_fields) {
      lines.push(`  ${f.field.padEnd(26)} ${fmtType(f)}`);
    }
  }
  lines.push('');
  lines.push('## Conditionally-required fields');
  lines.push('');
  if (md) {
    lines.push('| Field | Condition | Format |');
    lines.push('|---|---|---|');
    for (const f of r.conditional_fields) {
      lines.push(`| \`${f.key}\` | ${f.when} | \`${f.format}\` |`);
    }
  } else {
    for (const f of r.conditional_fields) {
      lines.push(`  ${f.key.padEnd(12)} when ${f.when}; format: ${f.format}`);
    }
  }
  lines.push('');
  lines.push('## Scope element (always present inside `<overview>`)');
  lines.push('');
  lines.push(`- \`includes\` — ${r.scope.includes}`);
  lines.push(`- \`excludes\` — ${r.scope.excludes}`);
  lines.push('');
  lines.push('## Delta element (wiki-style accumulation)');
  lines.push('');
  lines.push(`- Required when: ${r.delta.required_when}`);
  lines.push(`- Placement: ${r.delta.placement}`);
  lines.push(`- Ordering: ${r.delta.ordering}`);
  lines.push('');
  lines.push(`Last \`<delta>\` invariants:`);
  lines.push('');
  for (const [k, v] of Object.entries(r.delta.last_delta_invariants)) {
    lines.push(`- \`${k}\` — ${v}`);
  }
  lines.push('');
  lines.push('Per-child rules:');
  lines.push('');
  for (const [k, v] of Object.entries(r.delta.child_element_rules)) {
    lines.push(`- \`<${k}>\` — ${v}`);
  }
  lines.push('');
  lines.push('## Enforcement authority');
  lines.push('');
  lines.push('`scripts/lib/sdd-utils.js` — `parseSpecHeader` + `validateSpecHeader` are the sole');
  lines.push('enforcement. If this printed schema disagrees with the validator, the validator');
  lines.push('wins and this tool is wrong (file a bug).');
  return lines.join('\n');
}

function renderDecisionLog(opts = {}) {
  const md = !!opts.markdown;
  const r = DECISION_LOG_RULES;
  const lines = [];
  if (md) lines.push(...GENERATED_HEADER);
  lines.push('# decision-log Schema');
  lines.push('');
  lines.push(`Canonical path: ${fmtCode(r.canonical_path, md)}`);
  lines.push('');
  lines.push("The file lives inside brainstorming's date-stamped output directory");
  lines.push('(`docs/plans/<spec-id>/<YYYY-MM-DD>[-suffix]/`). The wire format (YAML, strict');
  lines.push('markdown table, etc.) is a deliberately unspecified implementation choice — pinning');
  lines.push('the format in the contract would over-constrain the implementation and freeze a');
  lines.push('swappable detail. The contract is the field shape and machine-parseability.');
  lines.push('');
  lines.push('## Required Fields per Row');
  lines.push('');
  lines.push(
    'All four fields MUST be present in every Q&A row. Missing any one field is an ERROR.',
  );
  lines.push('');
  if (md) {
    lines.push('| Field | Type | Description |');
    lines.push('|---|---|---|');
    for (const f of r.required_fields_per_row) {
      lines.push(`| \`${f.key}\` | ${f.type} | ${f.description} |`);
    }
  } else {
    for (const f of r.required_fields_per_row) {
      lines.push(`  ${f.key.padEnd(22)} ${f.type.padEnd(8)}  ${f.description}`);
    }
  }
  lines.push('');
  lines.push('## q_id Uniqueness Rule');
  lines.push('');
  lines.push(`${fmtCode('q_id', md)} ${r.q_id_uniqueness}.`);
  lines.push('');
  lines.push('## Row Addressability');
  lines.push('');
  lines.push(
    `Rows MUST be addressable by ${fmtCode(r.addressable_by, md)}. Lookup is a deterministic`,
  );
  lines.push("operation on the parsed structure — no LLM re-interpretation. Refiner's Phase 6");
  lines.push('mechanical authorization check iterates over every `<trace>` element citing a Q&A');
  lines.push("row by `q_id` and verifies the cited content appears in that row's");
  lines.push('`user_answer_verbatim`.');
  lines.push('');
  lines.push('## Deferral-Signal Canonical Phrases');
  lines.push('');
  lines.push('When `user_answer_verbatim` matches any of the following phrases (case-insensitive,');
  lines.push('trimmed), `deferral_signal` MUST be set to `true`:');
  lines.push('');
  for (const p of r.deferral_signal_canonical_phrases) {
    lines.push(`- ${fmtCode(p, md)}`);
  }
  lines.push('');
  lines.push(
    'Additional phrases MAY be recognized by implementations. The list above is the minimum',
  );
  lines.push('required set per `fr-cc-if-008-ac4`.');
  lines.push('');
  lines.push('## Examples');
  lines.push('');
  lines.push(
    'The schema-shape examples below illustrate the field contract. The wire format (YAML shown here) is one of several permitted choices; the contract is the field shape, not the wire encoding.',
  );
  lines.push('');
  lines.push('### Valid example');
  lines.push('');
  lines.push(
    'Three Q&A rows with distinct `q_id`s, mixing `deferral_signal=true` and `deferral_signal=false`:',
  );
  lines.push('');
  lines.push('```yaml');
  lines.push('- q_id: q1');
  lines.push('  question: "Which auth provider should the v2 spec target?"');
  lines.push('  user_answer_verbatim: "Auth0 — we already have an org account."');
  lines.push('  deferral_signal: false');
  lines.push('');
  lines.push('- q_id: q2');
  lines.push('  question: "Should refresh tokens rotate on every use?"');
  lines.push('  user_answer_verbatim: "use defaults"');
  lines.push('  deferral_signal: true');
  lines.push('');
  lines.push('- q_id: q3');
  lines.push('  question: "What is the session inactivity timeout?"');
  lines.push('  user_answer_verbatim: "30 minutes."');
  lines.push('  deferral_signal: false');
  lines.push('```');
  lines.push('');
  lines.push('### Invalid examples');
  lines.push('');
  lines.push(
    '1. Duplicate `q_id` within a session — `q1` reused for two different questions. Error: "duplicate q_id within a brainstorming session is ERROR" (per `q_id_uniqueness`).',
  );
  lines.push('');
  lines.push('```yaml');
  lines.push('- q_id: q1');
  lines.push('  question: "First question."');
  lines.push('  user_answer_verbatim: "Answer A."');
  lines.push('  deferral_signal: false');
  lines.push('');
  lines.push('- q_id: q1   # ← duplicate; MUST be q2');
  lines.push('  question: "Second question."');
  lines.push('  user_answer_verbatim: "Answer B."');
  lines.push('  deferral_signal: false');
  lines.push('```');
  lines.push('');
  lines.push(
    '2. Missing `deferral_signal` field. Error: "decision-log row missing required field: deferral_signal".',
  );
  lines.push('');
  lines.push('```yaml');
  lines.push('- q_id: q1');
  lines.push('  question: "Should we cache?"');
  lines.push('  user_answer_verbatim: "yes, redis"');
  lines.push('  # deferral_signal omitted ← ERROR');
  lines.push('```');
  lines.push('');
  lines.push(
    '3. Missing `user_answer_verbatim` field. Error: "decision-log row missing required field: user_answer_verbatim".',
  );
  lines.push('');
  lines.push('```yaml');
  lines.push('- q_id: q1');
  lines.push('  question: "Should we cache?"');
  lines.push('  # user_answer_verbatim omitted ← ERROR');
  lines.push('  deferral_signal: false');
  lines.push('```');
  lines.push('');
  lines.push('## Enforcement Authority');
  lines.push('');
  lines.push('`scripts/lib/sdd-rules.js` — `DECISION_LOG_RULES` is the canonical source of truth');
  lines.push('(re-exported via `scripts/lib/sdd-utils.js` for facade access). Validators built in');
  lines.push('`fr-sd-014` read from that constant. If this document disagrees with the constant,');
  lines.push('the constant wins and this document is wrong (file a bug).');
  return lines.join('\n');
}

function renderPendingConflict(opts = {}) {
  const md = !!opts.markdown;
  const r = PENDING_CONFLICT_RULES;
  const lines = [];
  if (md) lines.push(...GENERATED_HEADER);
  lines.push('# _pending-conflict.md Schema');
  lines.push('');
  lines.push(`Canonical path: ${fmtCode(r.canonical_path, md)}`);
  lines.push('');
  lines.push(
    'The underscore prefix marks the file as ephemeral hand-off, not versioned spec content.',
  );
  lines.push(
    'The file MUST NOT live under `details/` or any path that would be parsed as a spec detail file.',
  );
  lines.push('');
  lines.push('## Lifecycle');
  lines.push('');
  lines.push(`**State**: ${r.lifecycle.state} — non-versioned, non-authoritative.`);
  lines.push('');
  lines.push(`- **Written by**: ${r.lifecycle.written_by}.`);
  lines.push(`- **Read by**: ${r.lifecycle.read_by}.`);
  lines.push(`- **Deleted by**: ${r.lifecycle.deleted_by}.`);
  lines.push('');
  lines.push('Per A.1\'s narrowed Iron Law, this is the explicit ephemeral exception to "no');
  lines.push('authoritative state on block". Validators MUST treat persistence across a completed');
  lines.push(
    `conflict cycle as ${r.lifecycle.persist_across_completed_cycle}: \`"a prior conflict cycle did not complete cleanly"\`.`,
  );
  lines.push('');
  lines.push('## Required Fields');
  lines.push('');
  lines.push('All four fields MUST be present. Missing any one field is an ERROR.');
  lines.push('');
  if (md) {
    lines.push('| Field | Type | Description |');
    lines.push('|---|---|---|');
    for (const f of r.required_fields) {
      let typeStr = f.type;
      if (Array.isArray(f.allowed)) {
        typeStr = `enum (${f.allowed.map((v) => `\`"${v}"\``).join(', ')})`;
      } else if (typeof f.min_length === 'number' || typeof f.max_length === 'number') {
        typeStr = `${f.type}, length ${f.min_length}–${f.max_length}`;
      }
      lines.push(`| \`${f.key}\` | ${typeStr} | ${f.description} |`);
    }
  } else {
    for (const f of r.required_fields) {
      let typeStr = f.type;
      if (Array.isArray(f.allowed)) {
        typeStr = `${f.type} (${f.allowed.join('|')})`;
      } else if (typeof f.min_length === 'number' || typeof f.max_length === 'number') {
        typeStr = `${f.type}[${f.min_length}..${f.max_length}]`;
      }
      lines.push(`  ${f.key.padEnd(24)} ${typeStr.padEnd(28)}  ${f.description}`);
    }
  }
  lines.push('');
  lines.push('## Examples');
  lines.push('');
  lines.push('### Valid example');
  lines.push('');
  lines.push(
    'Axis 1 fired, two candidate resolutions are present, and the user-action prompt tells the user exactly what to choose.',
  );
  lines.push('');
  lines.push('```yaml');
  lines.push('axis_fired: "1"');
  lines.push(
    'conflict_description: "REQ-A says export is CSV-only, but the new design asks for JSON as the default."',
  );
  lines.push('candidate_resolutions:');
  lines.push('  - "Keep CSV-only and reject JSON default."');
  lines.push('  - "Change REQ-A to allow JSON default and record the compatibility impact."');
  lines.push(
    'user_action_prompt: "Choose one candidate resolution, or provide a new explicit requirement."',
  );
  lines.push('```');
  lines.push('');
  lines.push('### Invalid example');
  lines.push('');
  lines.push(
    'No candidate resolutions are present. Error: "_pending-conflict.md MUST contain at least one candidate resolution".',
  );
  lines.push('');
  lines.push('```yaml');
  lines.push('axis_fired: "1"');
  lines.push(
    'conflict_description: "Existing spec and new design disagree about the export format."',
  );
  lines.push('candidate_resolutions: []');
  lines.push('user_action_prompt: "Please resolve the conflict."');
  lines.push('```');
  lines.push('');
  lines.push('## Enforcement Authority');
  lines.push('');
  lines.push(
    '`scripts/lib/sdd-rules.js` — `PENDING_CONFLICT_RULES` is the canonical source of truth',
  );
  lines.push('(re-exported via `scripts/lib/sdd-utils.js` for facade access). Validators built in');
  lines.push('`fr-sd-014` read from that constant. If this document disagrees with the constant,');
  lines.push('the constant wins and this document is wrong (file a bug).');
  return lines.join('\n');
}

function renderVision(opts = {}) {
  const md = !!opts.markdown;
  const r = VISION_RULES;
  const lines = [];
  if (md) lines.push(...GENERATED_HEADER);
  lines.push('# Vision Schema');
  lines.push('');
  lines.push('Two-tier, date-less vision artifact — outside `DESIGN_DOC_RULES.path_regex`.');
  lines.push('LLM agents are read-only; only humans may modify these files.');
  lines.push('');
  lines.push('## Tiers');
  lines.push('');
  lines.push(`**Product vision**: ${fmtCode(r.product_canonical_path, md)}`);
  lines.push('');
  lines.push(
    'Single file, shared across all specs. Defines north-star product principles `P-1, P-2…`.',
  );
  lines.push('');
  lines.push(`**Spec vision**: ${fmtCode(r.spec_canonical_path, md)}`);
  lines.push('');
  lines.push('Per-spec scope chapter. References product principles via `principle_ref` fields.');
  lines.push('');
  lines.push('## Principle Identifier Pattern');
  lines.push('');
  lines.push(
    `Product vision principles are identified by: ${fmtRegex(r.principle_id_re.source, md)}`,
  );
  lines.push('');
  lines.push(
    'Examples: `P-1`, `P-12`. Principles appear at the start of a line (e.g. `P-1.` or `P-1:`).',
  );
  lines.push('');
  lines.push('## principle_ref');
  lines.push('');
  lines.push(
    `Field key: ${fmtCode(r.principle_ref_key, md)}. Per-spec decisions reference product principles using this key.`,
  );
  lines.push('');
  lines.push(
    '`validateVision` cross-checks that every `principle_ref` in a spec vision resolves to a `P-n` present in `product/vision.md`.',
  );
  lines.push('');
  lines.push('## LLM Access');
  lines.push('');
  lines.push(`${fmtCode(r.llm_access, md)} — vision files are human-written anchors.`);
  lines.push(
    'Agents MUST NOT write to vision files. Enforcement is via write-path blocking in the harness.',
  );
  lines.push('');
  lines.push('## Enforcement Authority');
  lines.push('');
  lines.push('`scripts/lib/sdd-rules.js` — `VISION_RULES` is the canonical source of truth');
  lines.push('(re-exported via `scripts/lib/sdd-utils.js`). `validateVision` in `sdd-utils.js`');
  lines.push('is the enforcement function. If this document disagrees with the constant,');
  lines.push('the constant wins (file a bug).');
  return lines.join('\n');
}

function renderDecisionLedger(opts = {}) {
  const md = !!opts.markdown;
  const r = DECISION_LEDGER_RULES;
  const lines = [];
  if (md) lines.push(...GENERATED_HEADER);
  lines.push('# Decision Ledger Schema');
  lines.push('');
  lines.push(`Canonical path: ${fmtCode(r.canonical_path, md)}`);
  lines.push('');
  lines.push(
    'The decision ledger is the cross-iteration strategic WHY log for a spec. ' +
      'It is distinct from the per-session `decision-log.yml` Q&A trace.',
  );
  lines.push('');
  lines.push('## Required Fields per Entry');
  lines.push('');
  lines.push(
    'All required fields MUST be present in every entry. Missing any one field is an ERROR.',
  );
  lines.push('');
  if (md) {
    lines.push('| Field | Type | Description |');
    lines.push('|---|---|---|');
    for (const f of r.required_fields) {
      lines.push(`| \`${f.key}\` | ${f.type} | ${f.description} |`);
    }
  } else {
    for (const f of r.required_fields) {
      lines.push(`  ${f.key.padEnd(20)} ${f.type.padEnd(8)}  ${f.description}`);
    }
  }
  lines.push('');
  lines.push('## Optional Fields per Entry');
  lines.push('');
  lines.push('These fields MAY appear in entries; absence is benign.');
  lines.push('');
  if (md) {
    lines.push('| Field | Type | Description |');
    lines.push('|---|---|---|');
    for (const f of r.optional_fields) {
      lines.push(`| \`${f.key}\` | ${f.type} | ${f.description} |`);
    }
  } else {
    for (const f of r.optional_fields) {
      lines.push(`  ${f.key.padEnd(16)} ${f.type.padEnd(8)}  ${f.description}`);
    }
  }
  lines.push('');
  lines.push('## Immutability Contract');
  lines.push('');
  lines.push(r.immutability);
  lines.push('');
  lines.push(
    'The `decision` and `why` fields are frozen once committed. ' +
      'Editing them against HEAD is ERROR. Corrections require a superseding entry.',
  );
  lines.push('');
  lines.push(
    'Known limitation: same-session pre-commit append-then-edit escapes the HEAD-relative check.',
  );
  lines.push('');
  lines.push('## D-id Monotonicity');
  lines.push('');
  lines.push(
    'D-ids follow the format `D-NNN` (e.g. `D-001`, `D-042`). ' +
      'They MUST be monotonically increasing and unique within a ledger. ' +
      'Non-increasing or duplicate D-id is ERROR.',
  );
  lines.push('');
  lines.push('## status Values');
  lines.push('');
  lines.push('- `proposed` — drafted, awaiting attended ratification.');
  lines.push('- `accepted` — ratified by a human via `arcforge ratify`.');
  lines.push('- `superseded-by:D-NNN` — superseded by a newer entry (open-ended string form).');
  lines.push('');
  lines.push('## Enforcement Authority');
  lines.push('');
  lines.push(
    '`scripts/lib/sdd-rules.js` — `DECISION_LEDGER_RULES` is the canonical source of truth',
  );
  lines.push(
    '(re-exported via `scripts/lib/sdd-utils.js`). `validateDecisionLedger` in `sdd-utils.js`',
  );
  lines.push('is the enforcement function. If this document disagrees with the constant,');
  lines.push('the constant wins (file a bug).');
  return lines.join('\n');
}

// Back-compat aliases (terminal-mode renderers) still exported under their old names
// so existing tests (schema SoT + drift-detection) keep working.
function renderDesignHuman() {
  return renderDesign({ markdown: false });
}
function renderSpecHuman() {
  return renderSpec({ markdown: false });
}
function renderDesignMarkdown() {
  return renderDesign({ markdown: true });
}
function renderSpecMarkdown() {
  return renderSpec({ markdown: true });
}

// Custom JSON serializer that replaces RegExp with a string form so the output
// round-trips through tools that expect plain JSON.
function jsonifyRules(rules) {
  return JSON.parse(
    JSON.stringify(rules, (_, v) => (v instanceof RegExp ? `<regex>${v.source}</regex>` : v)),
  );
}

const HELP = `print-schema.js — print the current SDD schema for an artifact target.

Usage:
  node scripts/lib/print-schema.js <target> [--markdown | --json]

Targets:
  design            Design doc schema (docs/plans/<spec-id>/<date>/design.md)
  spec              Spec identity header schema (specs/<spec-id>/spec.xml <overview>)
  decision-log      Structured decision-log schema (brainstorming Phase 2 output)
  pending-conflict  _pending-conflict.md handoff schema (refiner R3 block output)
  vision            Vision schema (product/vision.md + specs/<id>/vision.md)
  decision-ledger   Decision ledger schema (specs/<id>/decisions.yml)

Flags:
  --markdown  Emit the committed-markdown view. Wraps regex patterns in
              inline code fences and includes a "do not edit" header.
              Used to regenerate scripts/lib/sdd-schemas/*.md.
  --json      Emit JSON instead of text. RegExp values are encoded as
              <regex>PATTERN</regex> strings so the output is portable.
  --help      Show this help.

Rules source:
  scripts/lib/sdd-utils.js — DESIGN_DOC_RULES, SPEC_HEADER_RULES,
  DECISION_LOG_RULES, PENDING_CONFLICT_RULES, VISION_RULES, DECISION_LEDGER_RULES.
  This script NEVER duplicates structured schema fields. Valid/invalid
  examples for decision-log and pending-conflict live in renderer prose
  per fr-sd-013-ac3/fr-sd-012-ac3.`;

function main(argv) {
  const args = argv.slice(2);
  if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
    process.stdout.write(`${HELP}\n`);
    return 0;
  }
  const target = args[0];
  const asJson = args.includes('--json');
  const asMarkdown = args.includes('--markdown');
  if (asJson && asMarkdown) {
    process.stderr.write('Error: --json and --markdown are mutually exclusive.\n');
    return 1;
  }
  const RULES_BY_TARGET = {
    design: DESIGN_DOC_RULES,
    spec: SPEC_HEADER_RULES,
    'decision-log': DECISION_LOG_RULES,
    'pending-conflict': PENDING_CONFLICT_RULES,
    vision: VISION_RULES,
    'decision-ledger': DECISION_LEDGER_RULES,
  };
  const RENDERER_BY_TARGET = {
    design: renderDesign,
    spec: renderSpec,
    'decision-log': renderDecisionLog,
    'pending-conflict': renderPendingConflict,
    vision: renderVision,
    'decision-ledger': renderDecisionLedger,
  };
  const RULES = RULES_BY_TARGET[target] ?? null;
  const renderer = RENDERER_BY_TARGET[target] ?? null;
  if (!RULES || !renderer) {
    process.stderr.write(`Unknown target: ${target}\n\n${HELP}\n`);
    return 1;
  }
  let out;
  if (asJson) out = JSON.stringify(jsonifyRules(RULES), null, 2);
  else out = renderer({ markdown: asMarkdown });
  process.stdout.write(`${out}\n`);
  return 0;
}

if (require.main === module) {
  process.exit(main(process.argv));
}

module.exports = {
  renderDesign,
  renderSpec,
  renderDecisionLog,
  renderPendingConflict,
  renderVision,
  renderDecisionLedger,
  renderDesignHuman,
  renderSpecHuman,
  renderDesignMarkdown,
  renderSpecMarkdown,
  jsonifyRules,
};
