@opum-ai/lore 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -22
- package/bin/lore.cjs +35 -7
- package/package.json +17 -17
- package/src/adapters/backlog.ts +0 -1084
- package/src/adapters/git.ts +0 -221
- package/src/cli.ts +0 -667
- package/src/commands/agent.ts +0 -301
- package/src/commands/agents.ts +0 -302
- package/src/commands/args.ts +0 -209
- package/src/commands/changed.ts +0 -70
- package/src/commands/check.ts +0 -1031
- package/src/commands/codex-bridge.ts +0 -49
- package/src/commands/concurrency.ts +0 -48
- package/src/commands/context.ts +0 -292
- package/src/commands/discover.ts +0 -89
- package/src/commands/explorer.ts +0 -253
- package/src/commands/export.ts +0 -93
- package/src/commands/fswrite.ts +0 -928
- package/src/commands/graph.ts +0 -291
- package/src/commands/help.ts +0 -151
- package/src/commands/impact.ts +0 -59
- package/src/commands/init.ts +0 -583
- package/src/commands/instructions.ts +0 -91
- package/src/commands/link.ts +0 -929
- package/src/commands/new.ts +0 -476
- package/src/commands/orphans.ts +0 -457
- package/src/commands/path.ts +0 -67
- package/src/commands/provenance.ts +0 -68
- package/src/commands/query.ts +0 -312
- package/src/commands/reconcile-shared.ts +0 -280
- package/src/commands/rename.ts +0 -585
- package/src/commands/replace.ts +0 -320
- package/src/commands/scaffold.ts +0 -346
- package/src/commands/schema.ts +0 -293
- package/src/commands/snapshot.ts +0 -130
- package/src/commands/supersede.ts +0 -400
- package/src/commands/sync.ts +0 -371
- package/src/commands/tasks.ts +0 -271
- package/src/commands/traversal.ts +0 -151
- package/src/commands/validate.ts +0 -226
- package/src/config.ts +0 -598
- package/src/core/agent-bridge.ts +0 -287
- package/src/core/agent-context.ts +0 -498
- package/src/core/agent-profile.ts +0 -447
- package/src/core/bundle.ts +0 -893
- package/src/core/check.ts +0 -853
- package/src/core/codex-bridge.ts +0 -100
- package/src/core/concept.ts +0 -597
- package/src/core/consumer-scaffold.ts +0 -433
- package/src/core/context.ts +0 -271
- package/src/core/explorer-contract.ts +0 -441
- package/src/core/explorer-qualification.ts +0 -58
- package/src/core/explorer.ts +0 -518
- package/src/core/finding.ts +0 -31
- package/src/core/graph.ts +0 -201
- package/src/core/indexes.ts +0 -436
- package/src/core/instructions.ts +0 -209
- package/src/core/ladybug-driver.ts +0 -1795
- package/src/core/ladybug-lifecycle.ts +0 -1178
- package/src/core/ladybug-native.ts +0 -95
- package/src/core/ladybug-source.ts +0 -667
- package/src/core/links.ts +0 -681
- package/src/core/log.ts +0 -253
- package/src/core/managed-block.ts +0 -540
- package/src/core/manifest.ts +0 -718
- package/src/core/order.ts +0 -13
- package/src/core/profile.ts +0 -1007
- package/src/core/projection.ts +0 -195
- package/src/core/query.ts +0 -542
- package/src/core/reconcile.ts +0 -236
- package/src/core/replace.ts +0 -419
- package/src/core/retrieval.ts +0 -213
- package/src/core/rewrite.ts +0 -940
- package/src/core/scaffold.ts +0 -255
- package/src/core/schema.ts +0 -366
- package/src/core/snapshot-runtime.ts +0 -52
- package/src/core/snapshot-store.ts +0 -287
- package/src/core/snapshot.ts +0 -711
- package/src/core/template.ts +0 -429
- package/src/core/traversal.ts +0 -487
- package/src/core/validate.ts +0 -517
- package/src/core/workspace-contract.ts +0 -473
- package/src/core/workspace-projection.ts +0 -365
- package/src/core/workspace-retrieval.ts +0 -196
- package/src/core/workspace-source.ts +0 -174
- package/src/errors.ts +0 -697
- package/src/meta.ts +0 -7
- package/src/output.ts +0 -589
- package/src/scripts/upstream-backlog-watch.ts +0 -288
- package/src/state.ts +0 -390
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/** Shared CLI parsing and rendering for bounded typed traversal commands. */
|
|
2
|
-
|
|
3
|
-
import { idFromPath } from "../core/concept";
|
|
4
|
-
import {
|
|
5
|
-
DEFAULT_TRAVERSAL_LIMIT,
|
|
6
|
-
DEFAULT_TRAVERSAL_MAX_DEPTH,
|
|
7
|
-
type ImpactResult,
|
|
8
|
-
MAX_TRAVERSAL_DEPTH,
|
|
9
|
-
MAX_TRAVERSAL_LIMIT,
|
|
10
|
-
type PathResult,
|
|
11
|
-
type TraversalDirection,
|
|
12
|
-
type TraversalEndpointKind,
|
|
13
|
-
type TraversalSnapshot,
|
|
14
|
-
type TraversalStep,
|
|
15
|
-
} from "../core/traversal";
|
|
16
|
-
import { parseQualifiedWorkspaceId, qualifyWorkspaceId } from "../core/workspace-contract";
|
|
17
|
-
import { singleLine } from "../errors";
|
|
18
|
-
import type { Renderable } from "../output";
|
|
19
|
-
import { optionValues, type ParsedArgs, singleOptionValue, usage } from "./args";
|
|
20
|
-
|
|
21
|
-
export interface TraversalFlags {
|
|
22
|
-
readonly direction: TraversalDirection;
|
|
23
|
-
readonly edgeKinds?: readonly string[];
|
|
24
|
-
readonly maxDepth: number;
|
|
25
|
-
readonly limit: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function parseTraversalFlags(parsed: ParsedArgs): TraversalFlags {
|
|
29
|
-
const direction = requiredChoice(parsed, "direction", ["outbound", "inbound", "either"] as const);
|
|
30
|
-
const edgeKinds = optionValues(parsed, "edge").map((value) => requiredValue("edge", value));
|
|
31
|
-
if (new Set(edgeKinds).size !== edgeKinds.length) {
|
|
32
|
-
throw usage("--edge values must be unique", "pass each authored edge kind at most once");
|
|
33
|
-
}
|
|
34
|
-
const maxDepth = boundedInteger(parsed, "max-depth", DEFAULT_TRAVERSAL_MAX_DEPTH, 0, MAX_TRAVERSAL_DEPTH);
|
|
35
|
-
const limit = boundedInteger(parsed, "limit", DEFAULT_TRAVERSAL_LIMIT, 1, MAX_TRAVERSAL_LIMIT);
|
|
36
|
-
return {
|
|
37
|
-
direction,
|
|
38
|
-
...(edgeKinds.length > 0 ? { edgeKinds } : {}),
|
|
39
|
-
maxDepth,
|
|
40
|
-
limit,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function parseEndpointKind(parsed: ParsedArgs, name: string): TraversalEndpointKind {
|
|
45
|
-
return requiredChoice(parsed, name, ["concept", "task"] as const);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function normalizeEndpointId(raw: string, kind: TraversalEndpointKind, workspace: boolean): string {
|
|
49
|
-
const value = raw.trim();
|
|
50
|
-
if (value === "") throw usage("endpoint id must not be empty", "pass a concept or task id");
|
|
51
|
-
if (!workspace) return kind === "concept" ? idFromPath(value) : value;
|
|
52
|
-
try {
|
|
53
|
-
const parsed = parseQualifiedWorkspaceId(value);
|
|
54
|
-
return qualifyWorkspaceId(parsed.memberId, kind === "concept" ? idFromPath(parsed.sourceId) : parsed.sourceId);
|
|
55
|
-
} catch {
|
|
56
|
-
throw usage(`invalid workspace ${kind} id "${value}"`, "use the unambiguous <member-id>::<source-id> form");
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function assertKnownEdgeKinds(snapshot: TraversalSnapshot, requested?: readonly string[]): void {
|
|
61
|
-
if (requested === undefined) return;
|
|
62
|
-
const known = new Set(snapshot.edges.map((edge) => edge.kind));
|
|
63
|
-
const unknown = requested.find((kind) => !known.has(kind));
|
|
64
|
-
if (unknown !== undefined) {
|
|
65
|
-
throw usage(`unknown authored edge kind "${unknown}"`, "omit --edge to traverse all authored edge kinds");
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function pathRenderable(data: PathResult): Renderable<PathResult> {
|
|
70
|
-
return { kind: "path.result", data, pretty: renderPaths, plain: renderPaths };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function impactRenderable(data: ImpactResult): Renderable<ImpactResult> {
|
|
74
|
-
return {
|
|
75
|
-
kind: "impact.result",
|
|
76
|
-
data,
|
|
77
|
-
pretty: renderImpact,
|
|
78
|
-
plain: renderImpact,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function renderPaths(data: PathResult): string {
|
|
83
|
-
const header = `${data.shown} path${data.shown === 1 ? "" : "s"} from ${typed(data.from.kind, data.from.id)} to ${typed(data.to.kind, data.to.id)}`;
|
|
84
|
-
const lines = data.paths.map((path, index) => `${index + 1}. ${renderChain(path.edges, data.from.id)}`);
|
|
85
|
-
return `${[...lines, header, accounting(data)].join("\n")}\n`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function renderImpact(data: ImpactResult): string {
|
|
89
|
-
const lines = data.impacts.map(
|
|
90
|
-
(impact) =>
|
|
91
|
-
`${typed(impact.endpoint.kind, impact.endpoint.id)} ${impact.relationship} depth ${impact.depth} via ${renderChain(impact.evidence, data.root.id)}`,
|
|
92
|
-
);
|
|
93
|
-
const header = `${data.shown} impact${data.shown === 1 ? "" : "s"} from ${typed(data.root.kind, data.root.id)}`;
|
|
94
|
-
return `${[...lines, header, accounting(data)].join("\n")}\n`;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function renderChain(steps: readonly TraversalStep[], rootId: string): string {
|
|
98
|
-
if (steps.length === 0) return singleLine(rootId);
|
|
99
|
-
return [
|
|
100
|
-
singleLine(rootId),
|
|
101
|
-
...steps.flatMap((step) => [
|
|
102
|
-
step.direction === "outbound" ? `-${singleLine(step.edge.kind)}->` : `<-${singleLine(step.edge.kind)}-`,
|
|
103
|
-
singleLine(step.to.id),
|
|
104
|
-
]),
|
|
105
|
-
].join(" ");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function accounting(data: PathResult | ImpactResult): string {
|
|
109
|
-
return `depth<=${data.limits.maxDepth}, edge-visits=${data.edgeVisits}/${data.limits.maxEdgeVisits}, complete=${String(data.complete)}, truncated=${String(data.truncated)}`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function typed(kind: TraversalEndpointKind, id: string): string {
|
|
113
|
-
return `${kind}:${singleLine(id)}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function requiredValue(name: string, raw: string): string {
|
|
117
|
-
const value = raw.trim();
|
|
118
|
-
if (value === "") throw usage(`--${name} needs a value`, `pass --${name}=<value>`);
|
|
119
|
-
return value;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function requiredChoice<const T extends readonly string[]>(parsed: ParsedArgs, name: string, values: T): T[number] {
|
|
123
|
-
const raw = singleOptionValue(parsed, name);
|
|
124
|
-
if (raw === undefined || raw.trim() === "") {
|
|
125
|
-
throw usage(`--${name} is required`, `pass --${name} <${values.join("|")}>`);
|
|
126
|
-
}
|
|
127
|
-
const value = raw.trim();
|
|
128
|
-
if (!values.includes(value)) {
|
|
129
|
-
throw usage(`invalid --${name} "${value}"`, `choose one of ${values.join(", ")}`);
|
|
130
|
-
}
|
|
131
|
-
return value as T[number];
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function boundedInteger(
|
|
135
|
-
parsed: ParsedArgs,
|
|
136
|
-
name: string,
|
|
137
|
-
defaultValue: number,
|
|
138
|
-
minimum: number,
|
|
139
|
-
maximum: number,
|
|
140
|
-
): number {
|
|
141
|
-
const raw = singleOptionValue(parsed, name);
|
|
142
|
-
if (raw === undefined) return defaultValue;
|
|
143
|
-
if (!/^\d+$/u.test(raw)) {
|
|
144
|
-
throw usage(`invalid --${name} "${raw}"`, `pass an integer from ${minimum} through ${maximum}`);
|
|
145
|
-
}
|
|
146
|
-
const value = Number.parseInt(raw, 10);
|
|
147
|
-
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
148
|
-
throw usage(`--${name} must be between ${minimum} and ${maximum}`, `pass an integer no greater than ${maximum}`);
|
|
149
|
-
}
|
|
150
|
-
return value;
|
|
151
|
-
}
|
package/src/commands/validate.ts
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* commands/validate.ts — `lore validate [paths…]`: tiered per-file conformance reporting.
|
|
3
|
-
*
|
|
4
|
-
* The thin, side-effecting layer over the pure {@link validateFiles} engine (ADR-0007,
|
|
5
|
-
* cli-surface §validate): it parses the command's own arguments, **discovers** the files to
|
|
6
|
-
* check (a default whole-bundle walk, or the explicit `[paths…]` a pre-commit hook passes —
|
|
7
|
-
* LORE-19 AC#2), reads their bytes, asks core for the tiered findings, renders the report, and
|
|
8
|
-
* returns the exit code. All file discovery and I/O live here; all judgement lives in
|
|
9
|
-
* `core/validate.ts`.
|
|
10
|
-
*
|
|
11
|
-
* Unlike `init`/`new`, a content failure is **not** a thrown {@link LoreError}: `validate` is a
|
|
12
|
-
* reporter, so it emits the full `validate.report` on stdout and then *returns* exit `6` when any
|
|
13
|
-
* error-tier finding exists (or any warning under `--strict`) — the report is the payload, the
|
|
14
|
-
* exit code is the gate signal. Only a *usage* error (bad flag) or an *I/O* failure (an
|
|
15
|
-
* unreadable path) throws, funneling through the router's one error seam like every command.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { statSync } from "node:fs";
|
|
19
|
-
import { join, resolve } from "node:path";
|
|
20
|
-
import { walkMarkdown } from "../core/bundle";
|
|
21
|
-
import { loadProfile } from "../core/profile";
|
|
22
|
-
import { DOCS_DIR } from "../core/scaffold";
|
|
23
|
-
import { canonicalType } from "../core/schema";
|
|
24
|
-
import { type FileReport, type Finding, type ValidateReport, validateFiles } from "../core/validate";
|
|
25
|
-
import { ANSI, EXIT_CODES, EXIT_OK, ioError, LoreError, paint, WarningCollector, type Writer } from "../errors";
|
|
26
|
-
import { emit, type OutputContext, type Renderable } from "../output";
|
|
27
|
-
import { assertFlagAtMostOnce, parseCommandArgs, singleOptionValue } from "./args";
|
|
28
|
-
import { canonicalIdentity, readSource, toRepoRelative } from "./discover";
|
|
29
|
-
|
|
30
|
-
/** Options for {@link runValidate}; `root` and the streams are injectable for tests. */
|
|
31
|
-
export interface ValidateOptions {
|
|
32
|
-
/** The repo root the (relative) target paths resolve against. */
|
|
33
|
-
root: string;
|
|
34
|
-
/** The resolved output mode/color (from `output.ts`). */
|
|
35
|
-
output: OutputContext;
|
|
36
|
-
/** The command's normalized positional + flag tokens from Commander. */
|
|
37
|
-
args: readonly string[];
|
|
38
|
-
/** stdout sink; defaults to `process.stdout`. */
|
|
39
|
-
stdout?: Writer;
|
|
40
|
-
/** stderr sink for discovery advisories (a skipped symlink, an unreadable sub-directory); defaults to `process.stderr`. */
|
|
41
|
-
stderr?: Writer;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** The parsed form of `lore validate`'s arguments. */
|
|
45
|
-
interface ValidateArgs {
|
|
46
|
-
/** Explicit target paths (files or directories); empty means the whole bundle. */
|
|
47
|
-
paths: string[];
|
|
48
|
-
/** `--type <T>`: limit the report to one concept type (canonical or case-insensitive). */
|
|
49
|
-
type?: string;
|
|
50
|
-
/** `--strict`: treat any warning as a failure for the exit code. */
|
|
51
|
-
strict: boolean;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** A discovered file ready for the pure engine: its repo-relative path and raw bytes. */
|
|
55
|
-
interface SourceFile {
|
|
56
|
-
path: string;
|
|
57
|
-
raw: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Run `lore validate`: parse the arguments, discover and read the target files, validate them,
|
|
62
|
-
* emit the `validate.report`, and return the exit code — `0` when clean (or warnings-only without
|
|
63
|
-
* `--strict`), `6` when any error-tier finding exists (or any warning under `--strict`). A bad
|
|
64
|
-
* flag throws a `usage` {@link LoreError} (exit `2`); an unreadable path a `not_found`/`denied`.
|
|
65
|
-
*
|
|
66
|
-
* Discovery advisories (a `.md` concept skipped behind a symlink, an unreadable sub-directory) are
|
|
67
|
-
* flushed to stderr — never silently swallowed, so a run that omits files says so — but, like every
|
|
68
|
-
* advisory, they do not change the exit code.
|
|
69
|
-
*/
|
|
70
|
-
export function runValidate(options: ValidateOptions): number {
|
|
71
|
-
const parsed = parseValidateArgs(options.args);
|
|
72
|
-
const profile = loadProfile({ root: options.root });
|
|
73
|
-
const type = parsed.type === undefined ? undefined : canonicalType(parsed.type, profile);
|
|
74
|
-
const walkWarnings = new WarningCollector();
|
|
75
|
-
const files = collectFiles(options.root, parsed.paths, walkWarnings);
|
|
76
|
-
|
|
77
|
-
const report = validateFiles(files, type, profile);
|
|
78
|
-
emit(reportRenderable(report), options.output, options.stdout);
|
|
79
|
-
walkWarnings.flush({ color: options.output.color, stderr: options.stderr });
|
|
80
|
-
|
|
81
|
-
const failed = report.errorCount > 0 || (parsed.strict && report.warningCount > 0);
|
|
82
|
-
return failed ? EXIT_CODES.validation : EXIT_OK;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ── Argument parsing ───────────────────────────────────────────────────────────
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Parse `validate`'s tokens into target paths and its flags. Commander has already resolved
|
|
89
|
-
* lore's global flags, so anything `--`-prefixed here is a command flag: an unrecognized one is a
|
|
90
|
-
* `usage` error. Both `--flag value` and `--flag=value` forms are accepted; `--type`'s value must
|
|
91
|
-
* not itself be a flag-looking token (so a mis-ordered `--type --strict` fails loud rather than
|
|
92
|
-
* eating `--strict`). A `--` ends option parsing so a path may begin with `-`.
|
|
93
|
-
*/
|
|
94
|
-
function parseValidateArgs(args: readonly string[]): ValidateArgs {
|
|
95
|
-
const parsed = parseCommandArgs(args, "validate");
|
|
96
|
-
const type = singleOptionValue(parsed, "type");
|
|
97
|
-
assertFlagAtMostOnce(parsed, "strict");
|
|
98
|
-
if (type !== undefined && type.trim() === "") {
|
|
99
|
-
throw usage("`--type` needs a value", "pass a type, e.g. --type ADR");
|
|
100
|
-
}
|
|
101
|
-
return { paths: parsed.positionals, type, strict: parsed.flags.has("strict") };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ── File discovery ─────────────────────────────────────────────────────────────
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Discover and read every file to validate. With no explicit paths the target is the whole
|
|
108
|
-
* bundle (`docs/`); otherwise each path is taken verbatim — a **directory** is walked for `.md`
|
|
109
|
-
* files (via the same {@link walkMarkdown} the bundle loader uses, so the walk is sorted,
|
|
110
|
-
* symlink-safe, and `.md`-only — its skipped-symlink/unreadable-subdir advisories flow to
|
|
111
|
-
* `warnings`), and a **file** is read directly even if it is not `.md` (the user named it
|
|
112
|
-
* explicitly). Results are de-duplicated by the file's **canonical (realpath) identity**, so the
|
|
113
|
-
* same physical file named twice — including via two casings on a case-insensitive filesystem, or
|
|
114
|
-
* once directly and once under a walked directory — is validated and counted once.
|
|
115
|
-
*/
|
|
116
|
-
function collectFiles(root: string, paths: readonly string[], warnings: WarningCollector): SourceFile[] {
|
|
117
|
-
const targets = paths.length > 0 ? paths : [DOCS_DIR];
|
|
118
|
-
const files: SourceFile[] = [];
|
|
119
|
-
const seen = new Set<string>();
|
|
120
|
-
for (const target of targets) {
|
|
121
|
-
const abs = resolve(root, target);
|
|
122
|
-
for (const absFile of expandTarget(abs, target, warnings)) {
|
|
123
|
-
const identity = canonicalIdentity(absFile);
|
|
124
|
-
if (seen.has(identity)) {
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
seen.add(identity);
|
|
128
|
-
const repoRel = toRepoRelative(root, absFile);
|
|
129
|
-
files.push({ path: repoRel, raw: readSource(absFile, repoRel) });
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return files;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Expand one target to the absolute file paths it names: a directory to every `.md` under it (a
|
|
137
|
-
* sorted walk that does not follow symlinks *inside* the tree, routing its advisories to
|
|
138
|
-
* `warnings`), a file to itself. A path that does not exist is a `not_found` {@link LoreError}
|
|
139
|
-
* (exit `3`) naming the target the user gave, so a typo'd path fails loud rather than silently
|
|
140
|
-
* validating nothing. An explicitly-named directory *is* followed through a top-level symlink
|
|
141
|
-
* (the user named it); only links discovered beneath it are skipped, matching `loadBundle`.
|
|
142
|
-
*/
|
|
143
|
-
function expandTarget(abs: string, target: string, warnings: WarningCollector): string[] {
|
|
144
|
-
let stat: ReturnType<typeof statSync>;
|
|
145
|
-
try {
|
|
146
|
-
stat = statSync(abs);
|
|
147
|
-
} catch (cause) {
|
|
148
|
-
ioError(cause, {
|
|
149
|
-
denied: { message: `cannot access "${target}"`, hint: "check filesystem permissions on that path" },
|
|
150
|
-
notFound: { message: `path "${target}" does not exist`, hint: "check the path and try again" },
|
|
151
|
-
input: { path: target },
|
|
152
|
-
rethrowUnknown: true,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
if (stat.isDirectory()) {
|
|
156
|
-
return walkMarkdown(abs, warnings).map((rel) => join(abs, rel));
|
|
157
|
-
}
|
|
158
|
-
return [abs];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ── Output ─────────────────────────────────────────────────────────────────────
|
|
162
|
-
|
|
163
|
-
/** The per-result-type rendering bundle for `validate` (output.ts dispatches on the mode). */
|
|
164
|
-
function reportRenderable(data: ValidateReport): Renderable<ValidateReport> {
|
|
165
|
-
return {
|
|
166
|
-
kind: "validate.report",
|
|
167
|
-
data,
|
|
168
|
-
// Pretty and plain are the same layout; only color differs (plain is always ANSI-free), so a
|
|
169
|
-
// single renderer keyed on the color flag keeps the two views from ever drifting.
|
|
170
|
-
pretty: (report, opts) => renderReport(report, opts.color),
|
|
171
|
-
plain: (report) => renderReport(report, false),
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** One line per finding (colored by severity), a per-file `ok`/`skip` status otherwise, then a summary. */
|
|
176
|
-
function renderReport(data: ValidateReport, color: boolean): string {
|
|
177
|
-
const lines: string[] = [];
|
|
178
|
-
for (const file of data.files) {
|
|
179
|
-
lines.push(...fileLines(file, color));
|
|
180
|
-
}
|
|
181
|
-
lines.push(summaryLine(data, color));
|
|
182
|
-
return lines.join("\n");
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* The lines for one file: a single `ok`/`skip` status when there is nothing to report, else one
|
|
187
|
-
* line per finding (`error`/`warning` + path + `[rule]` + message). Color paints only the
|
|
188
|
-
* severity token, so the line stays diff-stable in plain mode.
|
|
189
|
-
*/
|
|
190
|
-
function fileLines(file: FileReport, color: boolean): string[] {
|
|
191
|
-
if (file.skipped) {
|
|
192
|
-
return [`${paint("skip", ANSI.dim, color)} ${file.path} (not a concept)`];
|
|
193
|
-
}
|
|
194
|
-
if (file.findings.length === 0) {
|
|
195
|
-
return [`${paint("ok", ANSI.green, color)} ${file.path}`];
|
|
196
|
-
}
|
|
197
|
-
return file.findings.map((finding) => findingLine(file.path, finding, color));
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** One finding line: `<severity> <path> [<rule>]: <message>`. */
|
|
201
|
-
function findingLine(path: string, finding: Finding, color: boolean): string {
|
|
202
|
-
const tone = finding.severity === "error" ? ANSI.red : ANSI.yellow;
|
|
203
|
-
return `${paint(finding.severity, tone, color)} ${path} [${finding.rule}]: ${finding.message}`;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** The trailing summary: file/error/warning/skip counts, the error count painted when nonzero. */
|
|
207
|
-
function summaryLine(data: ValidateReport, color: boolean): string {
|
|
208
|
-
const errors = `${data.errorCount} ${plural(data.errorCount, "error")}`;
|
|
209
|
-
const painted = data.errorCount > 0 ? paint(errors, ANSI.red, color) : errors;
|
|
210
|
-
return [
|
|
211
|
-
`${data.files.length} ${plural(data.files.length, "file")}`,
|
|
212
|
-
painted,
|
|
213
|
-
`${data.warningCount} ${plural(data.warningCount, "warning")}`,
|
|
214
|
-
`${data.skippedCount} skipped`,
|
|
215
|
-
].join(", ");
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** Pluralize a noun by count (`1 error`, `2 errors`). */
|
|
219
|
-
function plural(count: number, noun: string): string {
|
|
220
|
-
return count === 1 ? noun : `${noun}s`;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** A `usage` {@link LoreError} (exit `2`) with an actionable hint. */
|
|
224
|
-
function usage(message: string, hint: string): LoreError {
|
|
225
|
-
return new LoreError("usage", message, hint);
|
|
226
|
-
}
|