fix(cli): fail on error diagnostics (#11)

* fix(cli): fail on error diagnostics

* chore: release 1.6.1

* feat(init): refresh truth doc templates

* fix(init): preserve custom template preambles

* ci: limit GitHub token permissions

* ci: update setup-node action

---------

Co-authored-by: MerlinH <merlinh221@gmail.com>
This commit is contained in:
Merlin's Cat
2026-05-31 00:35:18 +10:00
committed by GitHub
co-authored by MerlinH
parent e5a586a420
commit 9e330a0fd3
202 changed files with 1290 additions and 354 deletions
+8
View File
@@ -38,13 +38,21 @@ type ContextOptions = OutputOptions & {
format?: string;
};
const markFailedWhenErrorDiagnosticsExist = (result: CommandResult): void => {
if (result.diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
process.exitCode = 1;
}
};
const writeResult = (result: CommandResult, options: OutputOptions): void => {
const output = options.json ? renderJson(result) : renderHuman(result);
process.stdout.write(`${output}\n`);
markFailedWhenErrorDiagnosticsExist(result);
};
const writeContextResult = (result: CommandResult, options: ContextOptions): void => {
if (!options.json && options.format === "markdown" && typeof result.data?.markdown === "string") {
process.stdout.write(result.data.markdown);
markFailedWhenErrorDiagnosticsExist(result);
return;
}
writeResult(result, options);
+41 -7
View File
@@ -3,7 +3,7 @@ import fg from "fast-glob";
import { DEFAULT_DOCS_HIERARCHY } from "../config/defaults.js";
import type { TruthmarkConfig } from "../config/schema.js";
import type { FileWriteResult } from "../fs/paths.js";
import { ensureRepoFile, resolveRepoPath } from "../fs/paths.js";
import { ensureRepoFile, resolveRepoPath, writeRepoFile } from "../fs/paths.js";
import type { Diagnostic } from "../output/diagnostic.js";
import { parseAreasMarkdown } from "../routing/areas.js";
import { resolveTruthDocsRoot } from "../truth/docs.js";
@@ -15,6 +15,7 @@ import {
TEST_BEHAVIOR_DOC_TEMPLATE_PATH,
WORKFLOW_DOC_TEMPLATE_PATH,
renderChildAreaTemplate,
mergeTruthDocTemplate,
renderArchitectureDocTemplateFile,
renderBehaviorDocTemplateFile,
renderContractDocTemplateFile,
@@ -70,6 +71,23 @@ const readBehaviorDocTemplate = async (rootDir: string): Promise<string> => {
}
};
const ensureOrUpdateTruthDocTemplate = async (
rootDir: string,
templatePath: string,
defaultTemplate: string,
): Promise<FileWriteResult> => {
const seededResult = await ensureRepoFile(rootDir, templatePath, defaultTemplate);
if (seededResult.status !== "unchanged") {
return seededResult;
}
const existingTemplate = await fs.readFile(resolveRepoPath(rootDir, templatePath), "utf8");
const mergedTemplate = mergeTruthDocTemplate(existingTemplate, defaultTemplate);
return writeRepoFile(rootDir, templatePath, mergedTemplate);
};
export const scaffoldHierarchy = async (
rootDir: string,
config: TruthmarkConfig,
@@ -106,26 +124,42 @@ export const scaffoldHierarchy = async (
),
);
results.push(
await ensureRepoFile(rootDir, BEHAVIOR_DOC_TEMPLATE_PATH, renderBehaviorDocTemplateFile()),
await ensureOrUpdateTruthDocTemplate(
rootDir,
BEHAVIOR_DOC_TEMPLATE_PATH,
renderBehaviorDocTemplateFile(),
),
);
results.push(
await ensureRepoFile(rootDir, CONTRACT_DOC_TEMPLATE_PATH, renderContractDocTemplateFile()),
await ensureOrUpdateTruthDocTemplate(
rootDir,
CONTRACT_DOC_TEMPLATE_PATH,
renderContractDocTemplateFile(),
),
);
results.push(
await ensureRepoFile(
await ensureOrUpdateTruthDocTemplate(
rootDir,
ARCHITECTURE_DOC_TEMPLATE_PATH,
renderArchitectureDocTemplateFile(),
),
);
results.push(
await ensureRepoFile(rootDir, WORKFLOW_DOC_TEMPLATE_PATH, renderWorkflowDocTemplateFile()),
await ensureOrUpdateTruthDocTemplate(
rootDir,
WORKFLOW_DOC_TEMPLATE_PATH,
renderWorkflowDocTemplateFile(),
),
);
results.push(
await ensureRepoFile(rootDir, OPERATIONS_DOC_TEMPLATE_PATH, renderOperationsDocTemplateFile()),
await ensureOrUpdateTruthDocTemplate(
rootDir,
OPERATIONS_DOC_TEMPLATE_PATH,
renderOperationsDocTemplateFile(),
),
);
results.push(
await ensureRepoFile(
await ensureOrUpdateTruthDocTemplate(
rootDir,
TEST_BEHAVIOR_DOC_TEMPLATE_PATH,
renderTestBehaviorDocTemplateFile(),
+347 -87
View File
@@ -212,6 +212,144 @@ export const WORKFLOW_DOC_TEMPLATE_PATH = "docs/templates/workflow-doc.md";
export const OPERATIONS_DOC_TEMPLATE_PATH = "docs/templates/operations-doc.md";
export const TEST_BEHAVIOR_DOC_TEMPLATE_PATH = "docs/templates/test-behavior-doc.md";
type TemplateSectionSpec = {
heading: string;
placeholder: string;
guidance: string[];
};
type ParsedTemplateSection = {
heading: string;
block: string;
};
const renderTemplateSection = (section: TemplateSectionSpec): string[] => {
return [
section.heading,
"",
"<!--",
...section.guidance,
"-->",
"",
`{{${section.placeholder}}}`,
"",
];
};
const titleToPlaceholder = (title: string): string => {
return title
.replace(/^#+\s+/u, "")
.toLowerCase()
.replaceAll(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
};
const findTemplateSectionHeadings = (template: string): Array<{ heading: string; index: number }> => {
const matches: Array<{ heading: string; index: number }> = [];
let fencedCodeMarker: "`" | "~" | null = null;
let fencedCodeLength = 0;
for (const lineMatch of template.matchAll(/^.*(?:\r?\n|$)/gm)) {
const rawLine = lineMatch[0];
if (rawLine.length === 0) {
continue;
}
const line = rawLine.replace(/\r?\n$/u, "");
const fenceMatch = /^(?: {0,3})(`{3,}|~{3,})/u.exec(line);
if (fenceMatch) {
const marker = fenceMatch[1]?.[0] as "`" | "~";
const length = fenceMatch[1]?.length ?? 0;
if (fencedCodeMarker === null) {
fencedCodeMarker = marker;
fencedCodeLength = length;
} else if (marker === fencedCodeMarker && length >= fencedCodeLength) {
fencedCodeMarker = null;
fencedCodeLength = 0;
}
continue;
}
if (fencedCodeMarker === null && /^## .+$/u.test(line)) {
matches.push({ heading: line.trim(), index: lineMatch.index });
}
}
return matches;
};
const parseTemplateSections = (template: string): { preamble: string; sections: ParsedTemplateSection[] } => {
const matches = findTemplateSectionHeadings(template);
if (matches.length === 0) {
return { preamble: template.trimEnd(), sections: [] };
}
const sections = matches.map((match, index) => {
const start = match.index;
const next = matches[index + 1];
const end = next?.index ?? template.length;
return {
heading: match.heading,
block: template.slice(start, end).trimEnd(),
};
});
return {
preamble: template.slice(0, matches[0]?.index ?? 0).trimEnd(),
sections,
};
};
export const mergeTruthDocTemplate = (existingTemplate: string, defaultTemplate: string): string => {
if (existingTemplate.trim().length === 0) {
return defaultTemplate;
}
const defaultParsed = parseTemplateSections(defaultTemplate);
const existingParsed = parseTemplateSections(existingTemplate);
const defaultHeadings = new Set(defaultParsed.sections.map((section) => section.heading));
const customBeforeDefault = new Map<string, ParsedTemplateSection[]>();
const trailingCustomSections: ParsedTemplateSection[] = [];
existingParsed.sections.forEach((section, index) => {
if (defaultHeadings.has(section.heading)) {
return;
}
const nextDefaultSection = existingParsed.sections
.slice(index + 1)
.find((candidate) => defaultHeadings.has(candidate.heading));
if (nextDefaultSection) {
const bucket = customBeforeDefault.get(nextDefaultSection.heading) ?? [];
bucket.push(section);
customBeforeDefault.set(nextDefaultSection.heading, bucket);
return;
}
trailingCustomSections.push(section);
});
const mergedSections = defaultParsed.sections.flatMap((section) => [
...(customBeforeDefault.get(section.heading) ?? []),
section,
]);
return [
existingParsed.preamble,
...mergedSections.map((section) => section.block),
...trailingCustomSections.map((section) => section.block),
"",
]
.filter((block) => block.length > 0)
.join("\n\n");
};
export const renderBehaviorDocTemplateFile = (): string => {
return [
"---",
@@ -227,92 +365,147 @@ export const renderBehaviorDocTemplateFile = (): string => {
"",
"## Purpose",
"",
"<!-- State why this feature exists, the user or system outcome it protects, and the problem it solves. Keep roadmap or implementation plans out of this section. -->",
"<!--",
"State the user/system outcome this behavior protects and why it exists.",
"Include the problem boundary and durable value; exclude roadmap, implementation plan, and historical narrative.",
"List the code, config, docs, or tests that support the claim in source_of_truth rather than prose-only assertion.",
"-->",
"",
"{{purpose}}",
"",
"## Scope",
"",
"{{scope}}",
"",
"<!--",
"This doc must own one coherent behavior surface.",
"Split into another leaf doc when content introduces:",
"- a distinct user or system outcome",
"- a separate lifecycle or state machine",
"- an unrelated rule family",
"- a different external contract",
"- code that should route through a different owner",
"Define the one coherent behavior surface this document owns.",
"Include in-scope actors, entrypoints, state/data owned by this doc, and explicit handoffs to neighboring truth docs.",
"Split into another leaf doc when content introduces a distinct outcome, state machine, rule family, external contract, or route owner.",
"Keep README.md files as indexes only.",
"-->",
"",
"{{scope}}",
"",
"This doc was created from the editable behavior-doc template at {{template_path}}.",
"",
"## Current Behavior",
"",
"<!-- Describe implemented behavior in present tense. Do not include desired future behavior. -->",
"<!--",
"Describe only current implemented behavior in present tense.",
"Cover observable behavior, important defaults, and user/system-visible effects; exclude desired future behavior and speculative design.",
"Every non-obvious claim should be checkable from source_of_truth evidence.",
"-->",
"",
"{{current_behavior}}",
"",
"## Core Rules",
"",
"<!-- Capture stable business rules, invariants, precedence rules, validation rules, and must-never constraints. Omit incidental implementation details. -->",
"<!--",
"Capture stable business rules, invariants, precedence rules, validation rules, and must-never constraints.",
"Separate rules from incidental implementation details; cite current implementation or tests for rule enforcement.",
"-->",
"",
"{{core_rules}}",
"",
"## Flows And States",
"",
"<!-- Use for route switches, state transitions, lifecycle stages, retries, fallbacks, and important error paths. Write 'None beyond current behavior.' when no distinct flow or state model exists. -->",
"<!--",
"Document state transitions, lifecycle stages, retries, fallbacks, route switches, and important error paths.",
"State 'None beyond current behavior.' when this behavior has no distinct flow or state model.",
"-->",
"",
"{{flows_and_states}}",
"",
"## Contracts",
"",
"<!-- Capture user-visible or integration contracts: CLI/API shape, inputs, outputs, diagnostics, files, events, permissions, or links to canonical contract docs. Avoid duplicating a separate canonical contract doc. -->",
"<!--",
"Capture user-visible or integration contracts: CLI/API shape, inputs, outputs, diagnostics, files, events, permissions, or links to canonical contract docs.",
"Avoid duplicating a separate canonical contract doc; link to it when contract ownership lives elsewhere.",
"-->",
"",
"{{contracts}}",
"",
"## Product Decisions",
"",
"<!-- Keep active decisions only. Replace stale decisions instead of appending historical logs. -->",
"<!--",
"Keep active decisions only, dated inline when added or changed.",
"Explain decisions that shape behavior, boundaries, rejected alternatives, or migration constraints; replace stale decisions instead of appending historical logs.",
"-->",
"",
"{{decision}}",
"",
"## Rationale",
"",
"<!-- Explain why the current behavior and active decisions are this way, including tradeoffs. -->",
"<!--",
"Explain why the current behavior and active decisions are this way, including tradeoffs and constraints.",
"Tie rationale to evidence-backed behavior; do not use this as a changelog.",
"-->",
"",
"{{rationale}}",
"",
"## Non-Goals",
"",
"<!-- Name adjacent behavior this doc intentionally does not own, especially tempting future expansions. -->",
"<!--",
"Name adjacent behavior this doc intentionally does not own, especially tempting future expansions or neighboring route owners.",
"Use this section to prevent scope creep and duplicate truth ownership.",
"-->",
"",
"{{non_goals}}",
"",
"## Maintenance Notes",
"",
"<!-- List related tests, routing cautions, migration notes, and common drift risks for future agents. Keep this operational, not historical. -->",
"<!--",
"List related tests, routing cautions, migration notes, evidence drift risks, and review triggers for future maintainers or agents.",
"Keep this operational and current-state focused, not historical.",
"-->",
"",
"{{maintenance_notes}}",
"",
].join("\n");
};
const sectionSpec = (
heading: string,
guidance: string[],
placeholder = titleToPlaceholder(heading),
): TemplateSectionSpec => ({ heading, guidance, placeholder });
const PURPOSE_SECTION = sectionSpec("## Purpose", [
"State the software-engineering outcome this document protects and why the documented surface exists.",
"Include durable value, impacted users/systems, and the problem boundary; exclude roadmap, implementation plans, and historical narrative.",
"Keep claims traceable to source_of_truth evidence rather than prose-only assertion.",
]);
const SCOPE_SECTION = sectionSpec("## Scope", [
"Define the one coherent surface this document owns, including actors, entrypoints, owned state/data, and handoffs to neighboring truth docs.",
"Call out important out-of-scope boundaries here or in Non-Goals; split the doc when it mixes distinct outcomes, lifecycles, contracts, or owners.",
]);
const PRODUCT_DECISIONS_SECTION = sectionSpec("## Product Decisions", [
"Keep active decisions only, dated inline when added or changed.",
"Capture decisions that shape behavior, interfaces, boundaries, compatibility, risk acceptance, or migration constraints.",
"Replace stale decisions instead of appending historical logs.",
], "decision");
const RATIONALE_SECTION = sectionSpec("## Rationale", [
"Explain why the current behavior, structure, or contract is this way, including tradeoffs and constraints.",
"Tie rationale to evidence-backed facts and active decisions; do not use this as a changelog.",
]);
const NON_GOALS_SECTION = sectionSpec("## Non-Goals", [
"Name adjacent behavior, responsibilities, interfaces, or future expansions this doc intentionally does not own.",
"Use this section to prevent scope creep and duplicate truth ownership.",
]);
const MAINTENANCE_NOTES_SECTION = sectionSpec("## Maintenance Notes", [
"List related tests, routing cautions, migration notes, compatibility risks, evidence drift risks, and review triggers for future maintainers or agents.",
"Keep this operational and current-state focused, not historical.",
]);
const renderTypedTruthDocTemplate = (
truthKind: string,
docType: string,
title: string,
sections: string[],
sections: TemplateSectionSpec[],
): string => {
const placeholderNameForSection = (section: string): string => {
return section
.replace(/^#+\s+/u, "")
.toLowerCase()
.replaceAll(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
};
return [
"---",
"status: active",
@@ -325,91 +518,158 @@ const renderTypedTruthDocTemplate = (
"",
`# ${title}`,
"",
"## Purpose",
"",
"{{purpose}}",
"",
"## Scope",
"",
"{{scope}}",
"",
...sections.flatMap((section) => [
section,
"",
`{{${placeholderNameForSection(section)}}}`,
"",
]),
"## Product Decisions",
"",
"{{decision}}",
"",
"## Rationale",
"",
"{{rationale}}",
"",
"## Non-Goals",
"",
"{{non_goals}}",
"",
"## Maintenance Notes",
"",
"{{maintenance_notes}}",
"",
...renderTemplateSection(PURPOSE_SECTION),
...renderTemplateSection(SCOPE_SECTION),
...sections.flatMap(renderTemplateSection),
...renderTemplateSection(PRODUCT_DECISIONS_SECTION),
...renderTemplateSection(RATIONALE_SECTION),
...renderTemplateSection(NON_GOALS_SECTION),
...renderTemplateSection(MAINTENANCE_NOTES_SECTION),
].join("\n");
};
export const renderContractDocTemplateFile = (): string => {
return renderTypedTruthDocTemplate("contract", "contract", "{{title}}", [
"## Contract Surface",
"## Inputs",
"## Outputs",
"## Errors And Diagnostics",
"## Compatibility Rules",
"## Versioning And Migration",
sectionSpec("## Contract Surface", [
"Identify the owned API, CLI, file format, event, protocol, permission boundary, or integration surface.",
"State consumers/producers, stability level, and the source files/tests that define the contract.",
]),
sectionSpec("## Inputs", [
"Document accepted parameters, payloads, files, environment/config keys, permissions, and validation rules.",
"Include required/optional status, defaults, constraints, and normalization behavior.",
]),
sectionSpec("## Outputs", [
"Document returned values, emitted files/events, state changes, side effects, and success diagnostics.",
"Make externally observable behavior explicit enough for compatibility review.",
]),
sectionSpec("## Errors And Diagnostics", [
"List error classes, exit/status codes, user-facing diagnostics, retries, and recoverability expectations.",
"Distinguish validation errors, dependency failures, authorization failures, and internal faults when applicable.",
]),
sectionSpec("## Compatibility Rules", [
"State backward/forward compatibility guarantees, tolerated inputs, deprecation rules, and breaking-change triggers.",
"Include compatibility tests or review gates that protect the contract.",
]),
sectionSpec("## Versioning And Migration", [
"Document version negotiation, schema/API version fields, rollout requirements, migration steps, and rollback expectations.",
"State 'Not versioned' only when the implementation truly has no versioning or migration surface.",
]),
]);
};
export const renderArchitectureDocTemplateFile = (): string => {
return renderTypedTruthDocTemplate("architecture", "architecture", "{{title}}", [
"## System Role",
"## Boundaries",
"## Components",
"## Data And Control Flow",
"## Ownership",
"## Cross-Cutting Constraints",
sectionSpec("## System Role", [
"Describe the current architectural role of this subsystem/component in the larger system.",
"State the primary responsibilities, consumers, providers, and why this boundary exists now.",
]),
sectionSpec("## Boundaries", [
"Define owned code/config/data, external dependencies, trust boundaries, and interfaces crossed by this architecture.",
"Name what is deliberately outside the boundary and link neighboring architecture or contract docs when they own it.",
]),
sectionSpec("## Components", [
"List the major runtime/build-time components, modules, services, jobs, or generated artifacts and their responsibilities.",
"Keep the component list current and evidence-backed; avoid speculative target architecture.",
]),
sectionSpec("## Data And Control Flow", [
"Describe important data movement, command/control paths, synchronization points, state ownership, and failure paths.",
"Call out persistence, queues, caches, external calls, and security-sensitive transitions where relevant.",
]),
sectionSpec("## Ownership", [
"Document team/module ownership, review responsibility, operational responsibility, and escalation paths if known.",
"If ownership is inferred from codeowners, config, or repository structure, cite that evidence.",
]),
sectionSpec("## Cross-Cutting Constraints", [
"Record active constraints such as security, privacy, reliability, performance, portability, maintainability, compliance, and cost.",
"Tie constraints to source evidence, tests, standards, or operational requirements where available.",
]),
]);
};
export const renderWorkflowDocTemplateFile = (): string => {
return renderTypedTruthDocTemplate("workflow", "behavior", "{{title}}", [
"## Triggers",
"## Inputs",
"## Execution Model",
"## Steps",
"## State, Retry, And Failure Behavior",
"## Outputs",
sectionSpec("## Triggers", [
"List events, commands, schedules, user actions, webhooks, or dependency signals that start this workflow.",
"Include preconditions, authorization requirements, debounce/coalescing behavior, and disabled states when applicable.",
]),
sectionSpec("## Inputs", [
"Document data, files, config, context, credentials, and environmental assumptions consumed by the workflow.",
"Include validation, defaults, and normalization that happen before execution.",
]),
sectionSpec("## Execution Model", [
"Describe synchronous/asynchronous execution, concurrency, locking, leases, batching, ordering, and idempotency behavior.",
"State whether the workflow is user-blocking, background, distributed, or delegated to another system.",
]),
sectionSpec("## Steps", [
"Capture the current ordered steps or phases at a level useful for maintenance and review.",
"Reference implementation entrypoints instead of duplicating line-by-line code behavior.",
]),
sectionSpec("## State, Retry, And Failure Behavior", [
"Document state transitions, retries, timeouts, compensation, fallback, partial-success, and terminal-failure behavior.",
"Make externally visible failure semantics and recovery responsibilities clear.",
]),
sectionSpec("## Outputs", [
"List artifacts, state changes, notifications, logs, metrics, diagnostics, and downstream triggers produced by the workflow.",
"Include success criteria and handoff points to other truth docs or systems.",
]),
]);
};
export const renderOperationsDocTemplateFile = (): string => {
return renderTypedTruthDocTemplate("operations", "behavior", "{{title}}", [
"## Operational Surface",
"## Runtime Topology",
"## Configuration",
"## Permissions",
"## Deployment And Rollback",
"## Availability And Observability",
sectionSpec("## Operational Surface", [
"Describe what operators, maintainers, or automated systems can observe or control for this surface.",
"Include commands, dashboards, alerts, runbooks, jobs, or operational APIs that define current operations.",
]),
sectionSpec("## Runtime Topology", [
"Document services, processes, containers, hosts, regions, dependencies, queues, stores, and network boundaries involved at runtime.",
"State single-node/local behavior explicitly when there is no distributed topology.",
]),
sectionSpec("## Configuration", [
"List operational config, environment variables, feature flags, secrets references, defaults, and reload/restart requirements.",
"Do not include secret values; describe storage and rotation expectations instead.",
]),
sectionSpec("## Permissions", [
"Document required identities, roles, scopes, filesystem/network permissions, and least-privilege boundaries.",
"Include user-facing authorization behavior and operator access requirements when relevant.",
]),
sectionSpec("## Deployment And Rollback", [
"Describe deployment mechanism, migration ordering, compatibility windows, rollback path, and known irreversible operations.",
"Call out manual gates, smoke checks, and post-deploy verification responsibilities.",
]),
sectionSpec("## Availability And Observability", [
"Capture availability expectations, health checks, metrics, logs, traces, alerts, SLO/error-budget signals, and known blind spots.",
"Include what maintainers should inspect first during incidents or degraded behavior.",
]),
]);
};
export const renderTestBehaviorDocTemplateFile = (): string => {
return renderTypedTruthDocTemplate("test-behavior", "behavior", "{{title}}", [
"## Test Surface",
"## Fixtures And Data Model",
"## Execution Model",
"## Assertions And Invariants",
"## Isolation Rules",
"## Reporting And Failure Semantics",
sectionSpec("## Test Surface", [
"Define the behavior, contract, architecture, or workflow surface these tests verify.",
"Link the canonical truth docs and code paths the tests are meant to protect.",
]),
sectionSpec("## Fixtures And Data Model", [
"Document fixtures, factories, seeds, mocks/fakes, test repositories, external-service substitutes, and data lifecycle rules.",
"Include cleanup, determinism, privacy, and cross-test contamination constraints.",
]),
sectionSpec("## Execution Model", [
"Describe how tests run: command, framework, parallelism, isolation, network/filesystem assumptions, and required services.",
"State whether tests are unit, integration, e2e, contract, smoke, regression, or generated checks.",
]),
sectionSpec("## Assertions And Invariants", [
"List the critical assertions, invariants, failure modes, and negative cases that make the tests meaningful.",
"Tie assertions to product/contract rules rather than incidental implementation details.",
]),
sectionSpec("## Isolation Rules", [
"Document transaction boundaries, temp directories, fake clocks, network blocking, shared resources, and teardown rules.",
"Call out known order dependencies or flake risks and how they are controlled.",
]),
sectionSpec("## Reporting And Failure Semantics", [
"Describe diagnostics, snapshots, logs, coverage signals, retry policy, and how maintainers should interpret failures.",
"Include escalation or quarantine criteria for flaky or environment-sensitive tests.",
]),
]);
};