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(" -> ")}`);
}
}
|