All files / src check-command.ts

100% Statements 36/36
84.84% Branches 28/33
100% Functions 10/10
100% Lines 35/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163                                                                          59x 59x       7x 7x           10x     10x                                                       155x 155x 4x   6x     151x 155x 151x               155x                                   20x 20x 1x         1x         19x 19x 19x   20x   19x 19x 6x 19x 3x 3x 3x     19x 1x 1x 1x       19x         6x                
import { resolve } from "node:path";
 
import {
  loadAllureResultsDirectory,
  type EvidenceAdapterResult,
  type UnmappedResult,
} from "./adapters/allure.js";
import { checkVerification } from "./check.js";
import type { EvidenceIssue, VerificationProjectCheckResult } from "./check.js";
import type { MouraManifest } from "./manifest.js";
import type { RequirementSourceLocation } from "./markdown.js";
import { safeDisplayString } from "./id.js";
import { loadProjectDirectory } from "./project.js";
 
export interface CheckCommandOutput {
  readonly exitCode: 0 | 1;
  readonly stdout: readonly string[];
  readonly stderr: readonly string[];
}
 
export interface CheckCommandDependencies {
  readonly loadEvidence?: (directory: string) => Promise<EvidenceAdapterResult>;
}
 
export interface CheckCommandOptions extends CheckCommandDependencies {
  readonly strictTraceability?: boolean;
}
 
export interface TraceabilityDiagnostic extends UnmappedResult {
  readonly code: "UNMAPPED";
  readonly severity: "warning" | "error";
  readonly message: string;
}
 
export function formatEvidenceAdapterIssue(
  issue: EvidenceAdapterResult["issues"][number],
): string {
  const source = issue.source === undefined ? "" : `${issue.source}: `;
  return `${issue.code}: ${source}${issue.message}`;
}
 
export function formatEvidenceIssue(issue: EvidenceIssue): string {
  const target = issue.canonicalId ?? "(no Case ID)";
  return `${issue.code}: ${target} [${issue.layer}]: ${issue.message}`;
}
 
export function formatTraceabilityDiagnostic(
  diagnostic: TraceabilityDiagnostic,
): string {
  const location = diagnostic.source
    ? `${safeDisplayString(diagnostic.name)} (${safeDisplayString(diagnostic.source)})`
    : safeDisplayString(diagnostic.name);
  return `${diagnostic.severity.toUpperCase()} ${diagnostic.code} ${location}: ${diagnostic.message}`;
}
 
export type ProjectCheckEvaluation =
  | {
      readonly kind: "invalid-project";
      readonly errors: readonly string[];
    }
  | {
      readonly kind: "checked";
      readonly manifest: MouraManifest;
      readonly requirementLocations: ReadonlyMap<
        string,
        RequirementSourceLocation
      >;
      readonly requirementSources: ReadonlyMap<string, string>;
      readonly specificationSources: ReadonlyMap<string, string>;
      readonly evidence: EvidenceAdapterResult["evidence"];
      readonly check: VerificationProjectCheckResult;
      readonly adapterIssues: EvidenceAdapterResult["issues"];
      readonly traceabilityDiagnostics: readonly TraceabilityDiagnostic[];
    };
 
/** Shared filesystem evaluation used by both human CLI and artifact renderers. */
export async function evaluateProjectDirectory(
  directory: string,
  options: CheckCommandOptions = {},
): Promise<ProjectCheckEvaluation> {
  const project = await loadProjectDirectory(directory);
  if (!project.manifest)
    return {
      kind: "invalid-project",
      errors: project.errors.map((problem) => problem.message),
    };
 
  const loadEvidence = options.loadEvidence ?? loadAllureResultsDirectory;
  const adapted = await loadEvidence(resolve(directory, "allure-results"));
  const traceabilityDiagnostics = (adapted.unmapped ?? []).map((result) => ({
    ...result,
    code: "UNMAPPED" as const,
    severity: options.strictTraceability
      ? ("error" as const)
      : ("warning" as const),
    message: "No Moura Case is associated with this test result.",
  }));
  return {
    kind: "checked",
    manifest: project.manifest,
    requirementLocations: project.requirementLocations ?? new Map(),
    requirementSources: project.requirementSources ?? new Map(),
    specificationSources: project.specificationSources ?? new Map(),
    evidence: adapted.evidence,
    check: checkVerification(project.manifest, adapted.evidence),
    adapterIssues: adapted.issues,
    traceabilityDiagnostics,
  };
}
 
/** Filesystem command boundary; the check core remains adapter-neutral and pure. */
export async function checkProjectDirectory(
  directory: string,
  options: CheckCommandOptions = {},
): Promise<CheckCommandOutput> {
  const evaluation = await evaluateProjectDirectory(directory, options);
  if (evaluation.kind === "invalid-project") {
    return {
      exitCode: 1,
      stdout: [],
      stderr: [
        "✗ Traceability validation failed",
        ...evaluation.errors.map((message) => `- ${message}`),
      ],
    };
  }
 
  const adapted = { issues: evaluation.adapterIssues };
  const checked = evaluation.check;
  const stdout = checked.entries.map(
    (entry) =>
      `${entry.status} ${entry.caseId} [${entry.layer}] (${entry.severity})`,
  );
  const stderr: string[] = [];
  for (const diagnostic of evaluation.traceabilityDiagnostics)
    stderr.push(formatTraceabilityDiagnostic(diagnostic));
  if (adapted.issues.length > 0) {
    stderr.push("✗ Evidence adapter issues");
    for (const issue of adapted.issues) {
      stderr.push(`- ${formatEvidenceAdapterIssue(issue)}`);
    }
  }
  if (checked.evidenceIssues.length > 0) {
    stderr.push("✗ Semantic evidence issues");
    for (const issue of checked.evidenceIssues) {
      stderr.push(`- ${formatEvidenceIssue(issue)}`);
    }
  }
 
  return {
    exitCode:
      adapted.issues.length === 0 &&
      checked.passed &&
      evaluation.traceabilityDiagnostics.every(
        (diagnostic) => diagnostic.severity !== "error",
      )
        ? 0
        : 1,
    stdout,
    stderr,
  };
}