import {
  createGitForPath,
  getGitForCwd
} from "./chunk-2UFQ3A3C.js";
import {
  validateAutomatedTestRequirement
} from "./chunk-U7I7FS7T.js";
import {
  PLACEHOLDER_SENTINEL,
  normalizeToDateString,
  parseYAML
} from "./chunk-NRIZR3A7.js";
import {
  BRANCHES,
  EMOJI,
  FILE_SYSTEM,
  GIT_COMMANDS,
  GIT_REFS,
  LOG_PREFIX,
  STRING_LITERALS,
  VALIDATION
} from "./chunk-DWMLTXKQ.js";
import {
  TEST_TYPES,
  WU_EXPOSURE,
  WU_STATUS,
  WU_TYPES,
  getConfig
} from "./chunk-V6OJGLBA.js";
import {
  ErrorCodes,
  createError,
  die
} from "./chunk-RXRKBBSM.js";

// ../core/dist/wu-done-validation.js
import path3 from "path";
import { existsSync as existsSync2, readFileSync } from "fs";
import { minimatch as minimatch2 } from "minimatch";

// ../core/dist/wu-rules-core.js
import path from "path";
import { minimatch } from "minimatch";
var RULE_CODES = {
  CODE_PATH_SHAPE: "R001_CODE_PATH_SHAPE",
  MINIMUM_TEST_INTENT: "R002_MINIMUM_TEST_INTENT",
  CODE_PATH_EXISTENCE: "R003_CODE_PATH_EXISTENCE",
  CODE_PATH_COVERAGE: "R004_CODE_PATH_COVERAGE",
  PARITY_MISSING_SURFACE: "R005_PARITY_MISSING_SURFACE",
  PARITY_UNAVAILABLE: "R005_PARITY_UNAVAILABLE",
  TEST_CLASSIFICATION: "R007_TEST_CLASSIFICATION",
  TEST_EXISTENCE: "R008_TEST_EXISTENCE"
};
var CLI_PACKAGE_JSON_PATH = "packages/@lumenflow/cli/package.json";
var REGISTRATION_SURFACES = {
  PUBLIC_MANIFEST: "packages/@lumenflow/cli/src/public-manifest.ts",
  MCP_TOOLS: "packages/@lumenflow/mcp/src/tools.ts"
};
var DEFAULT_HEAD_REF = "HEAD";
var AUTOMATED_TEST_BUCKETS = ["unit", "e2e", "integration"];
var BASIC_GLOB_CHAR_PATTERN = /[*?[\]{}]/;
var EXTGLOB_PATTERN = /[@!+*?]\(/;
function normalizeStringArray(value) {
  if (!Array.isArray(value)) {
    return [];
  }
  return value.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter(Boolean);
}
function normalizeCodePathsRaw(value) {
  if (!Array.isArray(value)) {
    return [];
  }
  return value;
}
function normalizeTests(testsValue) {
  const testsRecord = testsValue && typeof testsValue === "object" && !Array.isArray(testsValue) ? testsValue : {};
  return {
    manual: normalizeStringArray(testsRecord[TEST_TYPES.MANUAL]),
    unit: normalizeStringArray(testsRecord[TEST_TYPES.UNIT]),
    e2e: normalizeStringArray(testsRecord[TEST_TYPES.E2E]),
    integration: normalizeStringArray(testsRecord[TEST_TYPES.INTEGRATION])
  };
}
function normalizeContext(input) {
  const testsSource = input.tests && typeof input.tests === "object" ? input.tests : input.test_paths && typeof input.test_paths === "object" ? input.test_paths : {};
  const codePathsRaw = normalizeCodePathsRaw(input.code_paths);
  const codePaths = normalizeStringArray(input.code_paths);
  return {
    id: typeof input.id === "string" && input.id.trim() ? input.id.trim() : "(unknown-wu)",
    type: typeof input.type === "string" ? input.type : void 0,
    status: typeof input.status === "string" ? input.status : void 0,
    codePathsRaw,
    codePaths,
    tests: normalizeTests(testsSource),
    cwd: input.cwd?.trim() || process.cwd(),
    baseRef: input.baseRef,
    headRef: input.headRef || DEFAULT_HEAD_REF
  };
}
function isDocsOrProcess(type) {
  return type === WU_TYPES.DOCUMENTATION || type === WU_TYPES.PROCESS;
}
function hasGlobPattern(pathValue) {
  return BASIC_GLOB_CHAR_PATTERN.test(pathValue) || EXTGLOB_PATTERN.test(pathValue);
}
function normalizePathForCoverage(pathValue) {
  return pathValue.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/{2,}/g, "/").replace(/\/$/, "");
}
function isDirectoryLikeCodePath(codePath) {
  if (codePath.endsWith("/")) {
    return true;
  }
  const fileName = path.posix.basename(codePath);
  return !fileName.includes(".");
}
function addIssue(issues, issue) {
  issues.push(issue);
}
function isCodePathCoveredByChangedFiles(options) {
  const normalizedCodePath = normalizePathForCoverage(options.codePath);
  if (!normalizedCodePath) {
    return false;
  }
  const glob = hasGlobPattern(normalizedCodePath);
  const directoryLike = isDirectoryLikeCodePath(options.codePath);
  return options.changedFiles.some((changedFile) => {
    const normalizedChangedFile = normalizePathForCoverage(changedFile);
    if (!normalizedChangedFile) {
      return false;
    }
    if (normalizedChangedFile === normalizedCodePath) {
      return true;
    }
    if (glob) {
      return minimatch(normalizedChangedFile, normalizedCodePath, { dot: true });
    }
    if (directoryLike) {
      return normalizedChangedFile.startsWith(`${normalizedCodePath}/`);
    }
    return false;
  });
}
function findMissingCodePathCoverage(options) {
  const { codePaths, changedFiles } = options;
  return codePaths.filter((codePath) => !isCodePathCoveredByChangedFiles({ codePath, changedFiles }));
}
function isPathLikeTestEntry(value) {
  const trimmed = value.trim();
  if (!trimmed) {
    return false;
  }
  const hasWhitespace = /\s/.test(trimmed);
  const hasGlob = hasGlobPattern(trimmed);
  const hasFileSuffix = /(\.(test|spec)\.[A-Za-z0-9]+|\.[A-Za-z0-9]+)$/.test(trimmed);
  if (hasWhitespace && !trimmed.startsWith("./") && !trimmed.startsWith("../") && !trimmed.startsWith("/") && !hasGlob && !hasFileSuffix) {
    return false;
  }
  if (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("/")) {
    return true;
  }
  if (trimmed.includes("/") || trimmed.includes("\\")) {
    return true;
  }
  if (hasGlobPattern(trimmed)) {
    return true;
  }
  if (/\.(test|spec)\.[A-Za-z0-9]+$/i.test(trimmed)) {
    return true;
  }
  if (!trimmed.includes(" ") && /\.[A-Za-z0-9]+$/.test(trimmed)) {
    return true;
  }
  return false;
}
function validateRule001PathShape(context, issues) {
  context.codePathsRaw.forEach((entry, index) => {
    if (typeof entry !== "string" || !entry.trim()) {
      addIssue(issues, {
        code: RULE_CODES.CODE_PATH_SHAPE,
        severity: "error",
        message: `code_paths[${index}] must be a non-empty string path or glob.`,
        suggestion: "Provide a non-empty path/glob string or remove this entry."
      });
    }
  });
}
function validateRule002MinimumTestIntent(context, issues) {
  if (isDocsOrProcess(context.type)) {
    return;
  }
  if (context.codePaths.length === 0) {
    return;
  }
  const hasTestIntent = context.tests.manual.length > 0 || context.tests.unit.length > 0 || context.tests.e2e.length > 0 || context.tests.integration.length > 0;
  if (!hasTestIntent) {
    addIssue(issues, {
      code: RULE_CODES.MINIMUM_TEST_INTENT,
      severity: "error",
      message: "At least one test entry is required across tests.manual, tests.unit, tests.e2e, or tests.integration.",
      suggestion: "Add at least one test entry. Use tests.manual for descriptive checks when no automated path applies."
    });
  }
}
async function validateRule003CodePathExistence(context, issues, missingCodePaths, pathReferenceExists2) {
  const missing = [];
  for (const codePath of context.codePaths) {
    const exists = await pathReferenceExists2(codePath, context.cwd);
    if (!exists) {
      missing.push(codePath);
    }
  }
  if (missing.length > 0) {
    missingCodePaths.push(...missing);
    addIssue(issues, {
      code: RULE_CODES.CODE_PATH_EXISTENCE,
      severity: "error",
      message: `code_paths existence check failed for ${missing.length} path(s).`,
      suggestion: "Create the missing files/glob targets, or update code_paths to match actual repository paths.",
      metadata: { missingCodePaths: missing }
    });
  }
}
async function validateRule004Coverage(context, issues, changedFilesOutput, missingCoverageCodePaths, resolveChangedFiles2) {
  if (context.codePaths.length === 0) {
    return;
  }
  const changedFiles = await resolveChangedFiles2({
    cwd: context.cwd,
    baseRef: context.baseRef,
    headRef: context.headRef
  });
  if (!changedFiles.ok) {
    const coverageReason = "reason" in changedFiles ? changedFiles.reason : "git diff unavailable";
    missingCoverageCodePaths.push(...context.codePaths);
    addIssue(issues, {
      code: RULE_CODES.CODE_PATH_COVERAGE,
      severity: "error",
      message: `Unable to evaluate code_paths coverage: ${coverageReason}`,
      suggestion: "Ensure git diff base is available (origin/main or main) and rerun from the claimed worktree/branch context."
    });
    return;
  }
  changedFilesOutput.push(...changedFiles.files);
  const missing = findMissingCodePathCoverage({
    codePaths: context.codePaths,
    changedFiles: changedFiles.files
  });
  if (missing.length > 0) {
    missingCoverageCodePaths.push(...missing);
    addIssue(issues, {
      code: RULE_CODES.CODE_PATH_COVERAGE,
      severity: "error",
      message: `code_paths coverage failed: ${missing.length} scoped path(s) have no matching branch diff changes.`,
      suggestion: "Commit changes that touch each missing code_path, or update code_paths to match actual branch scope.",
      metadata: {
        missingCodePaths: missing,
        changedFiles: changedFiles.files,
        baseRef: changedFiles.baseRef,
        headRef: changedFiles.headRef
      }
    });
  }
}
function validateRule007AutomatedTestClassification(context, issues) {
  const pathLikeEntries = {
    unit: [],
    e2e: [],
    integration: []
  };
  for (const bucket of AUTOMATED_TEST_BUCKETS) {
    for (const entry of context.tests[bucket]) {
      if (!isPathLikeTestEntry(entry)) {
        addIssue(issues, {
          code: RULE_CODES.TEST_CLASSIFICATION,
          severity: "error",
          message: `tests.${bucket} entry is not path-like: "${entry}".`,
          suggestion: `Move descriptive text to tests.manual and keep tests.${bucket} for file paths/globs only.`,
          metadata: { bucket, value: entry }
        });
        continue;
      }
      pathLikeEntries[bucket].push(entry);
    }
  }
  return pathLikeEntries;
}
async function validateRule008AutomatedTestExistence(context, issues, pathLikeEntries, missingTestPaths, pathReferenceExists2) {
  const missing = [];
  for (const bucket of AUTOMATED_TEST_BUCKETS) {
    for (const testPath of pathLikeEntries[bucket]) {
      const exists = await pathReferenceExists2(testPath, context.cwd);
      if (!exists) {
        missing.push(testPath);
      }
    }
  }
  if (missing.length > 0) {
    missingTestPaths.push(...missing);
    addIssue(issues, {
      code: RULE_CODES.TEST_EXISTENCE,
      severity: "error",
      message: `Automated test path existence failed for ${missing.length} path(s).`,
      suggestion: "Create the missing automated test files/glob targets, or move non-path notes to tests.manual.",
      metadata: { missingTestPaths: missing }
    });
  }
}
async function validateRule005Parity(context, issues, resolveCliBinDiff2) {
  const includesCliPackage = context.codePaths.includes(CLI_PACKAGE_JSON_PATH);
  if (!includesCliPackage) {
    return { state: "unchanged", headRef: context.headRef };
  }
  const diff = await resolveCliBinDiff2({
    cwd: context.cwd,
    baseRef: context.baseRef,
    headRef: context.headRef
  });
  if (diff.state === "unavailable") {
    addIssue(issues, {
      code: RULE_CODES.PARITY_UNAVAILABLE,
      severity: "warning",
      message: `Skipped CLI registration parity check: ${diff.reason || "bin diff unavailable."}`,
      suggestion: "Ensure git base/head refs are available, then rerun reality validation to enforce parity.",
      metadata: {
        baseRef: diff.baseRef,
        headRef: diff.headRef
      }
    });
    return diff;
  }
  if (diff.state === "unchanged") {
    return diff;
  }
  const hasPublicManifest = context.codePaths.includes(REGISTRATION_SURFACES.PUBLIC_MANIFEST);
  const hasMcpTools = context.codePaths.includes(REGISTRATION_SURFACES.MCP_TOOLS);
  if (!hasPublicManifest) {
    addIssue(issues, {
      code: RULE_CODES.PARITY_MISSING_SURFACE,
      severity: "error",
      message: `CLI bin changed but '${REGISTRATION_SURFACES.PUBLIC_MANIFEST}' is missing from code_paths.`,
      suggestion: `Add '${REGISTRATION_SURFACES.PUBLIC_MANIFEST}' to code_paths for CLI registration parity.`,
      metadata: { surface: REGISTRATION_SURFACES.PUBLIC_MANIFEST, baseRef: diff.baseRef }
    });
  }
  if (!hasMcpTools) {
    addIssue(issues, {
      code: RULE_CODES.PARITY_MISSING_SURFACE,
      severity: "error",
      message: `CLI bin changed but '${REGISTRATION_SURFACES.MCP_TOOLS}' is missing from code_paths.`,
      suggestion: `Add '${REGISTRATION_SURFACES.MCP_TOOLS}' to code_paths for CLI registration parity.`,
      metadata: { surface: REGISTRATION_SURFACES.MCP_TOOLS, baseRef: diff.baseRef }
    });
  }
  return diff;
}
function finalizeValidation(issues, metadata) {
  const errors = issues.filter((issue) => issue.severity === "error");
  const warnings = issues.filter((issue) => issue.severity === "warning");
  return {
    valid: errors.length === 0,
    issues,
    errors,
    warnings,
    metadata
  };
}
function runCommonPhaseRules(context, issues) {
  validateRule001PathShape(context, issues);
  validateRule002MinimumTestIntent(context, issues);
}
function validateWURulesSync(input, options = {}) {
  const phase = options.phase || "structural";
  if (phase === "reality") {
    throw createError(ErrorCodes.INVALID_ARGUMENT, 'validateWURulesSync does not support phase "reality". Use validateWURulesWithResolvers.');
  }
  const context = normalizeContext(input);
  const issues = [];
  runCommonPhaseRules(context, issues);
  return finalizeValidation(issues, {
    missingCodePaths: [],
    missingCoverageCodePaths: [],
    missingTestPaths: [],
    changedFiles: [],
    parityState: "unavailable"
  });
}
async function validateWURulesWithResolvers(input, options = {}, resolvers) {
  const phase = options.phase || "structural";
  if (phase !== "reality") {
    return validateWURulesSync(input, { phase });
  }
  const context = normalizeContext(input);
  const issues = [];
  const missingCodePaths = [];
  const missingCoverageCodePaths = [];
  const missingTestPaths = [];
  const changedFiles = [];
  runCommonPhaseRules(context, issues);
  await validateRule003CodePathExistence(context, issues, missingCodePaths, resolvers.pathReferenceExists);
  await validateRule004Coverage(context, issues, changedFiles, missingCoverageCodePaths, resolvers.resolveChangedFiles);
  const parity = await validateRule005Parity(context, issues, resolvers.resolveCliBinDiff);
  const pathLikeEntries = validateRule007AutomatedTestClassification(context, issues);
  await validateRule008AutomatedTestExistence(context, issues, pathLikeEntries, missingTestPaths, resolvers.pathReferenceExists);
  return finalizeValidation(issues, {
    missingCodePaths,
    missingCoverageCodePaths,
    missingTestPaths,
    changedFiles,
    parityState: parity.state,
    parityReason: parity.reason
  });
}

// ../core/dist/wu-rules-resolvers.js
import { existsSync } from "fs";
import path2 from "path";
import { isDeepStrictEqual } from "util";
import fg from "fast-glob";
var DEFAULT_HEAD_REF2 = "HEAD";
var BASE_REF_CANDIDATES = [GIT_REFS.ORIGIN_MAIN, BRANCHES.MAIN];
var BASE_REF_UNAVAILABLE_REASON = `Unable to resolve git base ref (tried ${BASE_REF_CANDIDATES.join(", ")}).`;
var GLOB_IGNORE_PATTERNS = ["**/node_modules/**"];
var GLOB_INCLUDE_DOT_ENTRIES = true;
var GLOB_ONLY_FILES = false;
var GLOB_FOLLOW_SYMLINKS = false;
var GLOB_SUPPRESS_ERRORS = true;
var GLOB_REQUIRE_UNIQUE_MATCHES = true;
var PATH_NOT_FOUND_PATTERNS = [
  /does not exist in/i,
  /exists on disk, but not in/i,
  /path .* not in/i,
  /unknown revision or path/i,
  /fatal: path /i
];
function getGlobOptions(cwd) {
  return {
    cwd,
    dot: GLOB_INCLUDE_DOT_ENTRIES,
    onlyFiles: GLOB_ONLY_FILES,
    followSymbolicLinks: GLOB_FOLLOW_SYMLINKS,
    suppressErrors: GLOB_SUPPRESS_ERRORS,
    ignore: GLOB_IGNORE_PATTERNS,
    unique: GLOB_REQUIRE_UNIQUE_MATCHES
  };
}
function toErrorMessage(error) {
  if (error instanceof Error) {
    return error.message;
  }
  return String(error);
}
function splitLines(output) {
  return output.split("\n").map((line) => line.trim()).filter(Boolean);
}
function matchesAnyNotFoundPattern(message) {
  return PATH_NOT_FOUND_PATTERNS.some((pattern) => pattern.test(message));
}
async function readJsonFileAtRef(options) {
  const git = createGitForPath(options.cwd);
  try {
    const output = await git.raw(["show", `${options.ref}:${options.filePath}`]);
    const parsed = JSON.parse(output);
    return { ok: true, value: parsed };
  } catch (error) {
    const message = toErrorMessage(error);
    if (matchesAnyNotFoundPattern(message)) {
      return { ok: false, missing: true };
    }
    return { ok: false, missing: false, reason: message };
  }
}
function isMissingRefResult(result) {
  return !result.ok && "missing" in result && result.missing === true;
}
function isErrorRefResult(result) {
  return !result.ok && "missing" in result && result.missing === false;
}
function pathReferenceExistsSync(reference, cwd) {
  const normalizedReference = reference.trim();
  if (!normalizedReference) {
    return false;
  }
  if (hasGlobPattern(normalizedReference)) {
    try {
      const matches = fg.sync(normalizedReference, getGlobOptions(cwd));
      return matches.length > 0;
    } catch {
      return false;
    }
  }
  const fullPath = path2.join(cwd, normalizedReference);
  return existsSync(fullPath);
}
async function pathReferenceExists(reference, cwd) {
  const normalizedReference = reference.trim();
  if (!normalizedReference) {
    return false;
  }
  if (hasGlobPattern(normalizedReference)) {
    try {
      const matches = await fg(normalizedReference, getGlobOptions(cwd));
      return matches.length > 0;
    } catch {
      return false;
    }
  }
  const fullPath = path2.join(cwd, normalizedReference);
  return existsSync(fullPath);
}
async function resolveBaseRef(options) {
  if (options.baseRef && options.baseRef.trim()) {
    return options.baseRef.trim();
  }
  const git = createGitForPath(options.cwd);
  for (const candidateRef of BASE_REF_CANDIDATES) {
    try {
      if (await git.branchExists(candidateRef)) {
        return candidateRef;
      }
    } catch {
    }
  }
  return null;
}
async function resolveChangedFiles(options) {
  const cwd = options.cwd;
  const headRef = options.headRef || DEFAULT_HEAD_REF2;
  const baseRef = await resolveBaseRef({ cwd, baseRef: options.baseRef });
  if (!baseRef) {
    return {
      ok: false,
      reason: BASE_REF_UNAVAILABLE_REASON
    };
  }
  const git = createGitForPath(cwd);
  try {
    const output = await git.raw(["diff", "--name-only", `${baseRef}...${headRef}`]);
    const files = splitLines(output).map((filePath) => normalizePathForCoverage(filePath));
    return {
      ok: true,
      files,
      baseRef,
      headRef
    };
  } catch (error) {
    return {
      ok: false,
      reason: toErrorMessage(error)
    };
  }
}
async function resolveCliBinDiff(options) {
  const cwd = options.cwd;
  const headRef = options.headRef || DEFAULT_HEAD_REF2;
  const baseRef = await resolveBaseRef({ cwd, baseRef: options.baseRef });
  if (!baseRef) {
    return {
      state: "unavailable",
      reason: BASE_REF_UNAVAILABLE_REASON,
      headRef
    };
  }
  const baseDoc = await readJsonFileAtRef({ cwd, ref: baseRef, filePath: CLI_PACKAGE_JSON_PATH });
  const headDoc = await readJsonFileAtRef({ cwd, ref: headRef, filePath: CLI_PACKAGE_JSON_PATH });
  if (isErrorRefResult(headDoc)) {
    return {
      state: "unavailable",
      reason: headDoc.reason,
      baseRef,
      headRef
    };
  }
  if (isErrorRefResult(baseDoc)) {
    return {
      state: "unavailable",
      reason: baseDoc.reason,
      baseRef,
      headRef
    };
  }
  if (isMissingRefResult(baseDoc) && headDoc.ok) {
    return {
      state: "changed",
      reason: `${CLI_PACKAGE_JSON_PATH} does not exist at ${baseRef} but exists at ${headRef}.`,
      baseRef,
      headRef
    };
  }
  if (baseDoc.ok && isMissingRefResult(headDoc)) {
    return {
      state: "changed",
      reason: `${CLI_PACKAGE_JSON_PATH} exists at ${baseRef} but not at ${headRef}.`,
      baseRef,
      headRef
    };
  }
  if (isMissingRefResult(baseDoc) && isMissingRefResult(headDoc)) {
    return {
      state: "unavailable",
      reason: `${CLI_PACKAGE_JSON_PATH} is missing at both ${baseRef} and ${headRef}.`,
      baseRef,
      headRef
    };
  }
  if (!baseDoc.ok || !headDoc.ok) {
    return {
      state: "unavailable",
      reason: "Unable to resolve package.json state at base/head refs.",
      baseRef,
      headRef
    };
  }
  const baseBin = baseDoc.value.bin;
  const headBin = headDoc.value.bin;
  return {
    state: isDeepStrictEqual(baseBin, headBin) ? "unchanged" : "changed",
    baseRef,
    headRef
  };
}
function createDefaultWURuleResolvers() {
  return {
    pathReferenceExists,
    resolveChangedFiles,
    resolveCliBinDiff
  };
}

// ../core/dist/wu-validation.js
var LANE_PARENTS = {
  CONTENT: "content",
  FRAMEWORK: "framework",
  OPERATIONS: "operations"
};
function getLaneParent(lane) {
  if (!lane || typeof lane !== "string")
    return "";
  return (lane.split(":")[0] ?? "").trim().toLowerCase();
}
function resolveExposureDefault(lane) {
  const parent = getLaneParent(lane);
  if (parent === LANE_PARENTS.CONTENT) {
    return WU_EXPOSURE.DOCUMENTATION;
  }
  if (parent === LANE_PARENTS.FRAMEWORK || parent === LANE_PARENTS.OPERATIONS) {
    return WU_EXPOSURE.BACKEND_ONLY;
  }
  return void 0;
}
var UI_VERIFICATION_KEYWORDS = [
  "ui",
  "frontend",
  "component",
  "widget",
  "page",
  "displays",
  "shows",
  "renders",
  "user sees",
  "visible",
  "screen",
  "interface"
];
var EXPOSURE_WARNING_MESSAGES = {
  /**
   * Warning when exposure field is missing entirely.
   * @param {string} wuId - The WU identifier
   * @returns {string} Warning message with remediation
   */
  MISSING_EXPOSURE: (wuId) => `${wuId}: exposure field is missing. Add 'exposure: ui|api|backend-only|documentation' to the WU YAML. This helps ensure user-facing features have corresponding UI coverage.`,
  /**
   * Warning when API exposure lacks UI pairing WUs.
   * @param {string} wuId - The WU identifier
   * @returns {string} Warning message with remediation
   */
  MISSING_UI_PAIRING: (wuId) => `${wuId}: exposure=api but ui_pairing_wus not specified. Add 'ui_pairing_wus: [WU-XXX]' listing UI WUs that consume this API, or set exposure to 'backend-only' if no UI is planned.`,
  /**
   * Warning when API exposure lacks UI verification in acceptance criteria.
   * @param {string} wuId - The WU identifier
   * @returns {string} Warning message with remediation
   */
  MISSING_UI_VERIFICATION: (wuId) => `${wuId}: exposure=api but acceptance criteria lacks UI verification mention. Consider adding a criterion like 'UI displays the data correctly' to ensure end-to-end coverage.`,
  /**
   * Recommendation for user_journey when exposure is UI.
   * @param {string} wuId - The WU identifier
   * @returns {string} Warning message with remediation
   */
  MISSING_USER_JOURNEY: (wuId) => `${wuId}: exposure=ui but user_journey field not present. Adding 'user_journey: "<description>"' is recommended to document the user flow.`
};
function hasUIVerificationInAcceptance(acceptance) {
  let criteria;
  if (Array.isArray(acceptance)) {
    criteria = acceptance;
  } else {
    criteria = Object.values(acceptance).flat();
  }
  const lowerCriteria = criteria.map((c) => typeof c === "string" ? c.toLowerCase() : "");
  return lowerCriteria.some((criterion) => UI_VERIFICATION_KEYWORDS.some((keyword) => criterion.includes(keyword.toLowerCase())));
}
function validateExposure(wu, options = {}) {
  const warnings = [];
  if (options.skipExposureCheck) {
    return { valid: true, warnings: [] };
  }
  const wuId = wu.id || "WU-???";
  const exposure = wu.exposure || resolveExposureDefault(wu.lane);
  if (!exposure) {
    warnings.push(EXPOSURE_WARNING_MESSAGES.MISSING_EXPOSURE(wuId));
    return { valid: true, warnings };
  }
  if (exposure === WU_EXPOSURE.API) {
    const uiPairingWus = wu.ui_pairing_wus;
    if (!uiPairingWus || uiPairingWus.length === 0) {
      warnings.push(EXPOSURE_WARNING_MESSAGES.MISSING_UI_PAIRING(wuId));
    }
    const acceptance = wu.acceptance;
    if (acceptance && !hasUIVerificationInAcceptance(acceptance)) {
      warnings.push(EXPOSURE_WARNING_MESSAGES.MISSING_UI_VERIFICATION(wuId));
    }
  }
  if (exposure === WU_EXPOSURE.UI) {
    if (!wu.user_journey) {
      warnings.push(EXPOSURE_WARNING_MESSAGES.MISSING_USER_JOURNEY(wuId));
    }
  }
  return { valid: true, warnings };
}
var NAVIGATION_KEYWORDS = [
  "navigate",
  "navigation",
  "accessible",
  "access",
  "visible",
  "reachable",
  "go to",
  "visit",
  "open",
  "click",
  "link",
  "route",
  "url",
  "path",
  "/space",
  "/dashboard",
  "/settings"
];
var PAGE_FILE_PATTERNS = [
  /app\/.*\/page\.tsx$/,
  /app\/.*\/page\.ts$/,
  /pages\/.*\.tsx$/,
  /pages\/.*\.ts$/
];
var ACCESSIBILITY_ERROR_MESSAGES = {
  /**
   * Error when UI exposure lacks navigation accessibility proof.
   * @param {string} wuId - The WU identifier
   * @returns {string} Error message with remediation guidance
   */
  UI_NOT_ACCESSIBLE: (wuId) => `${wuId}: exposure=ui but feature accessibility not verified. Add one of the following:
  1. navigation_path: '/your-route' - specify the route where feature is accessible
  2. code_paths: [..., 'apps/web/src/app/.../page.tsx'] - include a page file
  3. tests.manual: ['Navigate to /path and verify feature is accessible'] - add navigation test

This prevents "orphaned code" - features that exist but users cannot access. Use --skip-accessibility-check to bypass (not recommended).`
};
function hasPageFileInCodePaths(codePaths) {
  if (!codePaths || !Array.isArray(codePaths)) {
    return false;
  }
  return codePaths.some((codePath) => PAGE_FILE_PATTERNS.some((pattern) => pattern.test(codePath)));
}
function hasNavigationInManualTests(tests) {
  if (!tests || typeof tests !== "object") {
    return false;
  }
  const manualTests = tests.manual;
  if (!manualTests || !Array.isArray(manualTests)) {
    return false;
  }
  const lowerTests = manualTests.map((t) => typeof t === "string" ? t.toLowerCase() : "");
  return lowerTests.some((test) => NAVIGATION_KEYWORDS.some((keyword) => test.includes(keyword.toLowerCase())));
}
function validateFeatureAccessibility(wu, options = {}) {
  const errors = [];
  if (options.skipAccessibilityCheck) {
    return { valid: true, errors: [] };
  }
  const exposure = wu.exposure;
  if (!exposure || exposure !== WU_EXPOSURE.UI) {
    return { valid: true, errors: [] };
  }
  const wuId = wu.id || "WU-???";
  if (wu.navigation_path && wu.navigation_path.trim().length > 0) {
    return { valid: true, errors: [] };
  }
  if (hasPageFileInCodePaths(wu.code_paths)) {
    return { valid: true, errors: [] };
  }
  if (hasNavigationInManualTests(wu.tests)) {
    return { valid: true, errors: [] };
  }
  errors.push(ACCESSIBILITY_ERROR_MESSAGES.UI_NOT_ACCESSIBLE(wuId));
  return { valid: false, errors };
}

// ../core/dist/file-classifiers.js
function ensureTrailingSlash(pathValue) {
  const normalized = pathValue.replace(/\\/g, "/").trim();
  if (!normalized) {
    return normalized;
  }
  return normalized.endsWith("/") ? normalized : `${normalized}/`;
}
function getDocsOnlyPrefixes(options = {}) {
  const directories = getConfig({ projectRoot: options.projectRoot }).directories;
  const prefixes = [directories.docs, directories.ai, directories.claude, directories.memoryBank].map(ensureTrailingSlash).filter((prefix) => prefix.length > 0);
  return Object.freeze(Array.from(new Set(prefixes)));
}
var DOCS_ONLY_ROOT_FILES = Object.freeze(["readme", "claude"]);
var TEST_FILE_PATTERNS = Object.freeze([
  /\.test\.(ts|tsx|js|jsx|mjs)$/,
  /\.spec\.(ts|tsx|js|jsx|mjs)$/,
  /__tests__\//,
  /\.test-utils\./,
  /\.mock\./
]);
function isMarkdownFile(filePath) {
  if (!filePath || typeof filePath !== "string") {
    return false;
  }
  const normalized = filePath.replace(/\\/g, "/");
  return /\.md$/i.test(normalized);
}
function isDocumentationPath(filePath) {
  if (!filePath || typeof filePath !== "string") {
    return false;
  }
  const path4 = filePath.trim();
  if (path4.length === 0) {
    return false;
  }
  for (const prefix of getDocsOnlyPrefixes()) {
    if (path4.startsWith(prefix)) {
      return true;
    }
  }
  if (isMarkdownFile(path4)) {
    const lowerPath = path4.toLowerCase();
    for (const pattern of DOCS_ONLY_ROOT_FILES) {
      if (lowerPath.startsWith(pattern)) {
        return true;
      }
    }
  }
  return false;
}

// ../core/dist/wu-done-validation.js
var GIT_TREE_LIST_ARGS = ["-r", "--name-only"];
var GLOB_MATCH_OPTIONS = { dot: true };
function applyExposureDefaults(doc) {
  if (!doc || typeof doc !== "object") {
    return { applied: false };
  }
  if (typeof doc.exposure === "string" && doc.exposure.trim().length > 0) {
    return { applied: false, exposure: doc.exposure };
  }
  const exposureDefault = resolveExposureDefault(doc.lane);
  if (!exposureDefault) {
    return { applied: false };
  }
  doc.exposure = exposureDefault;
  return { applied: true, exposure: exposureDefault };
}
async function validateCodePathsExist(doc, id, options = {}) {
  const { targetBranch = BRANCHES.MAIN, worktreePath = null } = options;
  const errors = [];
  const missing = [];
  const codePaths = Array.isArray(doc.code_paths) ? doc.code_paths.filter((entry) => typeof entry === "string") : [];
  if (codePaths.length === 0) {
    console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} No code_paths to validate for ${id}`);
    return { valid: true, errors: [], missing: [] };
  }
  console.log(`${LOG_PREFIX.DONE} Validating ${codePaths.length} code_paths exist...`);
  if (worktreePath && existsSync2(worktreePath)) {
    for (const filePath of codePaths) {
      const existsInWorktree = hasGlobPattern(filePath) ? pathReferenceExistsSync(filePath, worktreePath) : existsSync2(path3.join(worktreePath, filePath));
      if (!existsInWorktree) {
        missing.push(filePath);
      }
    }
    if (missing.length > 0) {
      errors.push(`code_paths validation failed - ${missing.length} file(s) not found in worktree:
${missing.map((p) => `  - ${p}`).join(STRING_LITERALS.NEWLINE)}

Ensure all files listed in code_paths exist before running wu:done.`);
    }
  } else {
    try {
      const gitAdapter = getGitForCwd();
      const branchFileListOutput = await gitAdapter.raw([
        GIT_COMMANDS.LS_TREE,
        ...GIT_TREE_LIST_ARGS,
        targetBranch
      ]);
      const branchFiles = branchFileListOutput.split(STRING_LITERALS.NEWLINE).map((entry) => entry.trim()).filter(Boolean);
      for (const filePath of codePaths) {
        if (hasGlobPattern(filePath)) {
          const hasGlobMatch = branchFiles.some((branchFile) => minimatch2(branchFile, filePath, GLOB_MATCH_OPTIONS));
          if (!hasGlobMatch) {
            missing.push(filePath);
          }
          continue;
        }
        try {
          const result = await gitAdapter.raw([GIT_COMMANDS.LS_TREE, targetBranch, "--", filePath]);
          if (!result || result.trim() === "") {
            missing.push(filePath);
          }
        } catch {
          missing.push(filePath);
        }
      }
      if (missing.length > 0) {
        errors.push(`code_paths validation failed - ${missing.length} file(s) not found on ${targetBranch}:
${missing.map((p) => `  - ${p}`).join(STRING_LITERALS.NEWLINE)}

\u274C POTENTIAL FALSE COMPLETION DETECTED

These files are listed in code_paths but do not exist on ${targetBranch}.
This prevents creating a stamp for incomplete work.

Fix options:
  1. Ensure all code is committed and merged to ${targetBranch}
  2. Update code_paths in ${id}.yaml to match actual files
  3. Remove files that were intentionally not created

Context: WU-1351 prevents false completions from INIT-WORKFLOW-INTEGRITY`);
      }
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not validate code_paths: ${message}`);
      return { valid: true, errors: [], missing: [] };
    }
  }
  if (errors.length > 0) {
    return { valid: false, errors, missing };
  }
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} All ${codePaths.length} code_paths verified`);
  return { valid: true, errors: [], missing: [] };
}
function validateSpecCompleteness(doc, _id) {
  const errors = [];
  if (doc.description && doc.description.includes(PLACEHOLDER_SENTINEL)) {
    errors.push(`Description contains ${PLACEHOLDER_SENTINEL} marker`);
  }
  if (doc.acceptance) {
    const hasPlaceholder = (value) => {
      if (typeof value === "string") {
        return value.includes(PLACEHOLDER_SENTINEL);
      }
      if (Array.isArray(value)) {
        return value.some((item) => hasPlaceholder(item));
      }
      if (typeof value === "object" && value !== null) {
        return Object.values(value).some((item) => hasPlaceholder(item));
      }
      return false;
    };
    if (hasPlaceholder(doc.acceptance)) {
      errors.push(`Acceptance criteria contain ${PLACEHOLDER_SENTINEL} markers`);
    }
  }
  if (!doc.description || doc.description.trim().length < VALIDATION.MIN_DESCRIPTION_LENGTH) {
    errors.push(`Description too short (${doc.description?.trim().length || 0} chars, minimum ${VALIDATION.MIN_DESCRIPTION_LENGTH})`);
  }
  if (doc.type !== WU_TYPES.DOCUMENTATION && doc.type !== WU_TYPES.PROCESS) {
    const codePaths = Array.isArray(doc.code_paths) ? doc.code_paths : [];
    if (codePaths.length === 0) {
      errors.push("Code paths required for non-documentation WUs");
    }
    const testObj = doc.tests && typeof doc.tests === "object" ? doc.tests : doc.test_paths && typeof doc.test_paths === "object" ? doc.test_paths : {};
    const hasItems = (arr) => Array.isArray(arr) && arr.length > 0;
    const hasUnitTests = hasItems(testObj[TEST_TYPES.UNIT]);
    const hasE2ETests = hasItems(testObj[TEST_TYPES.E2E]);
    const hasManualTests = hasItems(testObj[TEST_TYPES.MANUAL]);
    const hasIntegrationTests = hasItems(testObj[TEST_TYPES.INTEGRATION]);
    if (!(hasUnitTests || hasE2ETests || hasManualTests || hasIntegrationTests)) {
      errors.push("At least one test path required (unit, e2e, integration, or manual)");
    }
    const automatedTestResult = validateAutomatedTestRequirement(doc);
    if (!automatedTestResult.valid) {
      errors.push(...automatedTestResult.errors);
    }
  }
  return { valid: errors.length === 0, errors };
}
function deriveStatusFromEventsContent(eventsContent, wuId) {
  let status;
  for (const line of eventsContent.split("\n")) {
    if (!line.trim())
      continue;
    try {
      const event = JSON.parse(line);
      if (event.wuId !== wuId || !event.type)
        continue;
      switch (event.type) {
        case "claim":
        case "create":
          status = WU_STATUS.IN_PROGRESS;
          break;
        case "release":
          status = WU_STATUS.READY;
          break;
        case "complete":
          status = WU_STATUS.DONE;
          break;
        case "block":
          status = WU_STATUS.BLOCKED;
          break;
        case "unblock":
          status = WU_STATUS.IN_PROGRESS;
          break;
      }
    } catch {
    }
  }
  return status;
}
function validatePostMutation({ id, wuPath, stampPath, eventsPath = null }) {
  const errors = [];
  if (!existsSync2(stampPath)) {
    errors.push(`Stamp file not created: ${stampPath}`);
  }
  if (!existsSync2(wuPath)) {
    errors.push(`WU YAML not found after mutation: ${wuPath}`);
    return { valid: false, errors };
  }
  try {
    const content = readFileSync(wuPath, { encoding: FILE_SYSTEM.ENCODING });
    const doc = parseYAML(content);
    if (!doc.completed_at) {
      errors.push(`Missing required field 'completed_at' in ${id}.yaml`);
    } else {
      const timestamp = new Date(doc.completed_at);
      if (isNaN(timestamp.getTime())) {
        errors.push(`Invalid completed_at timestamp: ${doc.completed_at}`);
      }
    }
    if (!doc.completed) {
      errors.push(`Missing required field 'completed' in ${id}.yaml`);
    } else {
      const normalizedCompleted = normalizeToDateString(doc.completed);
      if (!normalizedCompleted) {
        errors.push(`Invalid completed date: ${doc.completed}`);
      } else if (normalizedCompleted !== doc.completed) {
        errors.push(`Non-normalized completed date: ${doc.completed}`);
      }
    }
    if (doc.locked !== true) {
      errors.push(`Missing or invalid 'locked' field in ${id}.yaml (expected: true, got: ${doc.locked})`);
    }
    if (doc.status !== WU_STATUS.DONE) {
      errors.push(`Invalid status in ${id}.yaml (expected: '${WU_STATUS.DONE}', got: '${doc.status}')`);
    }
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    errors.push(`Failed to parse WU YAML after mutation: ${message}`);
  }
  if (eventsPath) {
    if (!existsSync2(eventsPath)) {
      errors.push(`State store file not found after mutation: ${eventsPath}`);
    } else {
      try {
        const eventsContent = readFileSync(eventsPath, {
          encoding: FILE_SYSTEM.ENCODING
        });
        const derivedStatus = deriveStatusFromEventsContent(eventsContent, id);
        if (derivedStatus !== WU_STATUS.DONE) {
          errors.push(`WU ${id} state store is '${derivedStatus ?? "missing"}' after mutation (expected: '${WU_STATUS.DONE}')`);
        }
      } catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        errors.push(`Failed to parse state store after mutation: ${message}`);
      }
    }
  }
  return { valid: errors.length === 0, errors };
}
function validateTestPathsRequired(wu) {
  const wuId = typeof wu.id === "string" ? wu.id : "WU-unknown";
  if (wu.type === WU_TYPES.DOCUMENTATION || wu.type === WU_TYPES.PROCESS) {
    return { valid: true };
  }
  const codePaths = Array.isArray(wu.code_paths) ? wu.code_paths.filter((entry) => typeof entry === "string") : [];
  if (codePaths.length === 0) {
    return { valid: true };
  }
  const hasCodeChanges = codePaths.some((codePath) => !isDocumentationPath(codePath));
  if (!hasCodeChanges) {
    return { valid: true };
  }
  const testObj = wu.tests && typeof wu.tests === "object" ? wu.tests : {};
  const hasItems = (arr) => Array.isArray(arr) && arr.length > 0;
  const hasUnitTests = hasItems(testObj[TEST_TYPES.UNIT]);
  const hasE2ETests = hasItems(testObj[TEST_TYPES.E2E]);
  const hasManualTests = hasItems(testObj[TEST_TYPES.MANUAL]);
  const hasIntegrationTests = hasItems(testObj[TEST_TYPES.INTEGRATION]);
  if (!(hasUnitTests || hasE2ETests || hasManualTests || hasIntegrationTests)) {
    return {
      valid: false,
      error: `${wuId} requires test_paths: WU has code_paths but no tests specified. Add unit, e2e, integration, or manual tests.`
    };
  }
  const automatedTestResult = validateAutomatedTestRequirement(wu);
  if (!automatedTestResult.valid) {
    const errorSummary = automatedTestResult.errors[0]?.split("\n")[0] || "Automated tests required";
    return {
      valid: false,
      error: `${wuId}: ${errorSummary}`
    };
  }
  return { valid: true };
}
function isAllowedDocsPath(filePath) {
  if (!filePath || typeof filePath !== "string")
    return false;
  const normalized = filePath.replace(/\\/g, "/").trim();
  if (normalized.length === 0)
    return false;
  if (normalized.startsWith(".lumenflow/stamps/")) {
    return true;
  }
  return isDocumentationPath(normalized);
}
function validateTypeVsCodePathsPreflight(wu) {
  const errors = [];
  const blockedPaths = [];
  const wuId = typeof wu.id === "string" ? wu.id : "WU-unknown";
  if (wu.type !== WU_TYPES.DOCUMENTATION) {
    return { valid: true, errors: [], blockedPaths: [], abortedBeforeTransaction: false };
  }
  const codePaths = wu.code_paths;
  if (!codePaths || !Array.isArray(codePaths) || codePaths.length === 0) {
    return { valid: true, errors: [], blockedPaths: [], abortedBeforeTransaction: false };
  }
  for (const filePath of codePaths) {
    if (!isAllowedDocsPath(filePath)) {
      blockedPaths.push(filePath);
    }
  }
  if (blockedPaths.length > 0) {
    const pathsList = blockedPaths.map((p) => `  - ${p}`).join("\n");
    errors.push(`Documentation WU ${wuId} has code_paths that would fail pre-commit hook:
${pathsList}`);
    return { valid: false, errors, blockedPaths, abortedBeforeTransaction: true };
  }
  return { valid: true, errors: [], blockedPaths: [], abortedBeforeTransaction: false };
}
function buildTypeVsCodePathsErrorMessage(id, blockedPaths) {
  return `
PREFLIGHT VALIDATION FAILED (WU-2310)

WU ${id} is type: documentation but has code_paths that are not allowed:

${blockedPaths.map((p) => `  - ${p}`).join("\n")}

This would fail at git commit time (pre-commit hook: gateDocsOnlyPathEnforcement).
Aborting BEFORE transaction to prevent inconsistent state.

Fix options:

  1. Change WU type to 'engineering' (or 'feature', 'bug', etc.):
     pnpm wu:edit --id ${id} --type engineering

  2. Update code_paths to only include documentation files:
     pnpm wu:edit --id ${id} --code-paths "<docs-dir>/..." "*.md"

Allowed paths for documentation WUs:
  - configured docs-only prefixes from workspace.yaml software_delivery.directories
    (docs, ai, claude, memoryBank)
  - .lumenflow/stamps/
  - *.md files

After fixing, retry: pnpm wu:done --id ${id}
`;
}
async function validateCodePathsCommittedBeforeDone(wu, gitAdapter, options = {}) {
  const { abortOnFailure = true } = options;
  const errors = [];
  const uncommittedPaths = [];
  const codePaths = wu.code_paths;
  if (!codePaths || codePaths.length === 0) {
    return { valid: true, errors: [], uncommittedPaths: [] };
  }
  try {
    const gitStatus = await gitAdapter.getStatus();
    const statusLines = gitStatus.split("\n").filter((line) => line.trim());
    const uncommittedFiles = /* @__PURE__ */ new Set();
    for (const line of statusLines) {
      const match = line.match(/^.{2}\s+(.+)$/);
      if (match && match[1]) {
        const filePath = match[1];
        uncommittedFiles.add(filePath);
      }
    }
    for (const codePath of codePaths) {
      if (uncommittedFiles.has(codePath)) {
        uncommittedPaths.push(codePath);
      }
    }
    if (uncommittedPaths.length > 0) {
      const count = uncommittedPaths.length;
      const pathList = uncommittedPaths.map((p) => `  - ${p}`).join("\n");
      errors.push(`${count} code_path${count === 1 ? "" : "s"} are not committed:
${pathList}`);
      if (abortOnFailure) {
        const errorMessage = buildCodePathsCommittedErrorMessage(wu.id, uncommittedPaths);
        die(errorMessage);
      }
    }
  } catch (err) {
    console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not validate code_paths commit status: ${err instanceof Error ? err.message : String(err)}`);
    return { valid: true, errors: [], uncommittedPaths: [] };
  }
  return {
    valid: errors.length === 0,
    errors,
    uncommittedPaths
  };
}
function buildCodePathsCommittedErrorMessage(wuId, uncommittedPaths) {
  const count = uncommittedPaths.length;
  const pathList = uncommittedPaths.map((p) => `  - ${p}`).join("\n");
  return `
\u274C UNCOMMITTED CODE_PATHS DETECTED (WU-1153)

${count} code_path${count === 1 ? "" : "s"} for ${wuId} are not committed:

${pathList}

wu:done cannot proceed because uncommitted code_paths would be lost 
if the metadata transaction fails and needs to roll back.

This prevents lost work from metadata rollbacks after code commits.

Required actions:
  1. Commit your code changes:
     git add ${uncommittedPaths.join(" ")}
     git commit -m "implement: ${wuId} changes"

  2. Retry wu:done:
     pnpm wu:done --id ${wuId}

The guard ensures atomic completion: either both code and metadata succeed,
or neither is modified. This prevents partial state corruption.

Context: WU-1153 prevents lost work from metadata rollbacks
`;
}

export {
  getDocsOnlyPrefixes,
  DOCS_ONLY_ROOT_FILES,
  TEST_FILE_PATTERNS,
  RULE_CODES,
  REGISTRATION_SURFACES,
  isCodePathCoveredByChangedFiles,
  findMissingCodePathCoverage,
  validateWURulesSync,
  validateWURulesWithResolvers,
  resolveChangedFiles,
  createDefaultWURuleResolvers,
  validateExposure,
  validateFeatureAccessibility,
  applyExposureDefaults,
  validateCodePathsExist,
  validateSpecCompleteness,
  validatePostMutation,
  validateTestPathsRequired,
  validateTypeVsCodePathsPreflight,
  buildTypeVsCodePathsErrorMessage,
  validateCodePathsCommittedBeforeDone,
  buildCodePathsCommittedErrorMessage
};
