All files / src id.ts

96.15% Statements 50/52
91.66% Branches 33/36
100% Functions 11/11
95.65% Lines 44/46

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        48x     48x   48x   48x             153x 153x                 12657x 4x   12653x 15x         12638x 13x   12625x 12625x 12504x               13095x 104x 12991x 87277x 87277x 40x 40x 21x 87237x 22x     12948x         47x 47x       21x 26x     31x           119x 119x 1x 118x 354x       118x         5108x       11070x 10985x       5108x 5108x   11070x   1x      
import { stripVTControlCharacters } from "node:util";
 
import type { NodeKind, TraceNode } from "./model.js";
 
const SEPARATOR = "/";
 
/** Control code points are not portable across Moura's serialization boundaries. */
const UNICODE_CONTROL = /\p{Cc}/u;
/** Characters that can alter terminal or logical display structure. */
const UNSAFE_DISPLAY_CHARACTER = /[\p{Cc}\p{Bidi_Control}\u2028\u2029]/u;
const UNSAFE_DISPLAY_CHARACTER_GLOBAL =
  /[\p{Cc}\p{Bidi_Control}\u2028\u2029]/gu;
 
export type LocalId = string;
export type CanonicalId = string;
 
export class InvalidLocalIdError extends Error {
  constructor(localId: string, reason: string) {
    super(`Invalid local ID ${JSON.stringify(localId)}: ${reason}`);
    this.name = "InvalidLocalIdError";
  }
}
 
/**
 * Applies the invariant shared by every node kind. Naming patterns remain a
 * configuration concern so the core is not coupled to REQ/SCN/CASE prefixes.
 */
export function localId(value: string): LocalId {
  if (value.length === 0) {
    throw new InvalidLocalIdError(value, "must not be empty");
  }
  if (value.includes(SEPARATOR)) {
    throw new InvalidLocalIdError(
      value,
      `must not contain reserved separator '${SEPARATOR}'`,
    );
  }
  if (/\p{White_Space}/u.test(value)) {
    throw new InvalidLocalIdError(value, "must not contain whitespace");
  }
  const unsafeReason = interoperableStringError(value);
  if (unsafeReason) throw new InvalidLocalIdError(value, unsafeReason);
  return value;
}
 
/**
 * Returns why a Moura identifier-like string cannot safely cross its supported
 * UTF-8, YAML, Markdown, HTML, and tooling boundaries.
 */
export function interoperableStringError(value: string): string | undefined {
  if (UNICODE_CONTROL.test(value))
    return "must not contain Unicode control characters (General_Category=Cc)";
  for (let index = 0; index < value.length; index += 1) {
    const codeUnit = value.charCodeAt(index);
    if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
      const next = value.charCodeAt(index + 1);
      if (next >= 0xdc00 && next <= 0xdfff) index += 1;
      else return "must not contain unpaired UTF-16 surrogates";
    } else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
      return "must not contain unpaired UTF-16 surrogates";
    }
  }
  return undefined;
}
 
/** Make untrusted text safe for a single diagnostic/display boundary. */
export function safeDisplayString(value: string): string {
  const withoutTerminalControls = stripVTControlCharacters(value);
  if (
    interoperableStringError(withoutTerminalControls) === undefined &&
    !UNSAFE_DISPLAY_CHARACTER.test(withoutTerminalControls)
  )
    return withoutTerminalControls;
  return JSON.stringify(withoutTerminalControls).replace(
    UNSAFE_DISPLAY_CHARACTER_GLOBAL,
    (character) =>
      `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`,
  );
}
 
/** Validate an external canonical Case ID without constructing domain nodes. */
export function canonicalCaseIdError(value: string): string | undefined {
  const segments = value.split(SEPARATOR);
  if (segments.length !== 3)
    return "must contain exactly three local IDs separated by '/'";
  try {
    for (const segment of segments) localId(segment);
  } catch (error) {
    return error instanceof Error ? error.message : String(error);
  }
  return undefined;
}
 
/** The single construction point for logical canonical IDs. */
export function canonicalId(nodes: readonly TraceNode[]): CanonicalId {
  Iif (nodes.length === 0) {
    throw new Error("A canonical ID requires at least one node");
  }
 
  assertHierarchy(nodes.map((node) => node.kind));
  return nodes.map((node) => localId(node.localId)).join(SEPARATOR);
}
 
function assertHierarchy(kinds: readonly NodeKind[]): void {
  const hierarchy: readonly NodeKind[] = ["requirement", "scenario", "case"];
  if (
    kinds.length > hierarchy.length ||
    kinds.some((kind, index) => kind !== hierarchy[index])
  ) {
    throw new Error(`Invalid node hierarchy: ${kinds.join(" -> ")}`);
  }
}