@opum-ai/lore 0.1.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/LICENSE +21 -0
- package/README.md +306 -0
- package/bin/lore.cjs +109 -0
- package/package.json +67 -0
- package/src/adapters/backlog.ts +1084 -0
- package/src/adapters/git.ts +221 -0
- package/src/cli.ts +667 -0
- package/src/commands/agent.ts +301 -0
- package/src/commands/agents.ts +302 -0
- package/src/commands/args.ts +209 -0
- package/src/commands/changed.ts +70 -0
- package/src/commands/check.ts +1031 -0
- package/src/commands/codex-bridge.ts +49 -0
- package/src/commands/concurrency.ts +48 -0
- package/src/commands/context.ts +292 -0
- package/src/commands/discover.ts +89 -0
- package/src/commands/explorer.ts +253 -0
- package/src/commands/export.ts +93 -0
- package/src/commands/fswrite.ts +928 -0
- package/src/commands/graph.ts +291 -0
- package/src/commands/help.ts +151 -0
- package/src/commands/impact.ts +59 -0
- package/src/commands/init.ts +583 -0
- package/src/commands/instructions.ts +91 -0
- package/src/commands/link.ts +929 -0
- package/src/commands/new.ts +476 -0
- package/src/commands/orphans.ts +457 -0
- package/src/commands/path.ts +67 -0
- package/src/commands/provenance.ts +68 -0
- package/src/commands/query.ts +312 -0
- package/src/commands/reconcile-shared.ts +280 -0
- package/src/commands/rename.ts +585 -0
- package/src/commands/replace.ts +320 -0
- package/src/commands/scaffold.ts +346 -0
- package/src/commands/schema.ts +293 -0
- package/src/commands/snapshot.ts +130 -0
- package/src/commands/supersede.ts +400 -0
- package/src/commands/sync.ts +371 -0
- package/src/commands/tasks.ts +271 -0
- package/src/commands/traversal.ts +151 -0
- package/src/commands/validate.ts +226 -0
- package/src/config.ts +598 -0
- package/src/core/agent-bridge.ts +287 -0
- package/src/core/agent-context.ts +498 -0
- package/src/core/agent-profile.ts +447 -0
- package/src/core/bundle.ts +893 -0
- package/src/core/check.ts +853 -0
- package/src/core/codex-bridge.ts +100 -0
- package/src/core/concept.ts +597 -0
- package/src/core/consumer-scaffold.ts +433 -0
- package/src/core/context.ts +271 -0
- package/src/core/explorer-contract.ts +441 -0
- package/src/core/explorer-qualification.ts +58 -0
- package/src/core/explorer.ts +518 -0
- package/src/core/finding.ts +31 -0
- package/src/core/graph.ts +201 -0
- package/src/core/indexes.ts +436 -0
- package/src/core/instructions.ts +209 -0
- package/src/core/ladybug-driver.ts +1795 -0
- package/src/core/ladybug-lifecycle.ts +1178 -0
- package/src/core/ladybug-native.ts +95 -0
- package/src/core/ladybug-source.ts +667 -0
- package/src/core/links.ts +681 -0
- package/src/core/log.ts +253 -0
- package/src/core/managed-block.ts +540 -0
- package/src/core/manifest.ts +718 -0
- package/src/core/order.ts +13 -0
- package/src/core/profile.ts +1007 -0
- package/src/core/projection.ts +195 -0
- package/src/core/query.ts +542 -0
- package/src/core/reconcile.ts +236 -0
- package/src/core/replace.ts +419 -0
- package/src/core/retrieval.ts +213 -0
- package/src/core/rewrite.ts +940 -0
- package/src/core/scaffold.ts +255 -0
- package/src/core/schema.ts +366 -0
- package/src/core/snapshot-runtime.ts +52 -0
- package/src/core/snapshot-store.ts +287 -0
- package/src/core/snapshot.ts +711 -0
- package/src/core/template.ts +429 -0
- package/src/core/traversal.ts +487 -0
- package/src/core/validate.ts +517 -0
- package/src/core/workspace-contract.ts +473 -0
- package/src/core/workspace-projection.ts +365 -0
- package/src/core/workspace-retrieval.ts +196 -0
- package/src/core/workspace-source.ts +174 -0
- package/src/errors.ts +697 -0
- package/src/meta.ts +7 -0
- package/src/output.ts +589 -0
- package/src/scripts/upstream-backlog-watch.ts +288 -0
- package/src/state.ts +390 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/graph.ts — `lore graph [<id>] [--dot] [--depth <n>]`.
|
|
3
|
+
*
|
|
4
|
+
* The thin, read-only layer that emits the bundle's cross-link graph (cli-surface
|
|
5
|
+
* §graph; LORE-31). Commander supplies a verified indexed {@link BundleGraph}
|
|
6
|
+
* with automatic reference fallback; direct core callers retain the reference
|
|
7
|
+
* loader. The selected graph is then shaped as follows:
|
|
8
|
+
*
|
|
9
|
+
* - with **no `<id>`** exports the whole bundle;
|
|
10
|
+
* - with an `<id>` exports the **subgraph** rooted there, bounded to `--depth`
|
|
11
|
+
* hops ({@link subgraph}; unbounded when `--depth` is omitted).
|
|
12
|
+
*
|
|
13
|
+
* Output follows the uniform CLI modes: a human node/edge listing at a TTY (or
|
|
14
|
+
* piped plain), and the `{schemaVersion, kind: "graph.export", data}` envelope
|
|
15
|
+
* under the global `--json` — there is no command-specific JSON flag, so machine
|
|
16
|
+
* consumers use the same `--json` they use everywhere. `--dot` is the one
|
|
17
|
+
* representation override: it emits Graphviz DOT instead of the listing, so `lore
|
|
18
|
+
* graph --dot | dot -Tpng` works (a piped stdout auto-selects plain). `--dot` and
|
|
19
|
+
* `--json` are mutually exclusive — DOT has no envelope form.
|
|
20
|
+
*
|
|
21
|
+
* The positional `<id>` is normalized through {@link idFromPath} exactly as `lore
|
|
22
|
+
* rename`/`supersede` normalize theirs, so a path-form, `./`-prefixed, or
|
|
23
|
+
* `.md`-suffixed id resolves to the same bundle key (consistent id acceptance
|
|
24
|
+
* across the id-taking commands).
|
|
25
|
+
*
|
|
26
|
+
* Validation lives here (the byte/shape computation stays pure in `core/graph.ts`
|
|
27
|
+
* and `core/query.ts`): an unknown flag, a repeated/value-less or value-bearing
|
|
28
|
+
* `--dot`, a repeated/value-less/non-integer/too-large `--depth`, a `--depth`
|
|
29
|
+
* without a root `<id>`, `--dot` together with `--json`, or a stray second
|
|
30
|
+
* positional is a `usage` error (exit 2); a root `<id>` absent from the bundle
|
|
31
|
+
* surfaces as the `not_found` error (exit 3) {@link subgraph} throws.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
import type { BacklogAdapter } from "../adapters/backlog";
|
|
36
|
+
import { loadBundle } from "../core/bundle";
|
|
37
|
+
import { idFromPath } from "../core/concept";
|
|
38
|
+
import { buildGraphExport, type GraphExport, toDot } from "../core/graph";
|
|
39
|
+
import { loadProfile } from "../core/profile";
|
|
40
|
+
import { subgraph } from "../core/query";
|
|
41
|
+
import { loadRetrievalGraph, type RetrievalGraphLoader } from "../core/retrieval";
|
|
42
|
+
import { DOCS_DIR } from "../core/scaffold";
|
|
43
|
+
import { parseQualifiedWorkspaceId, qualifyWorkspaceId } from "../core/workspace-contract";
|
|
44
|
+
import type { WorkspaceRetrievalContext, WorkspaceRetrievalSelection } from "../core/workspace-retrieval";
|
|
45
|
+
import { EXIT_OK, LoreError, singleLine, WarningCollector, type Writer } from "../errors";
|
|
46
|
+
import { emit, type OutputContext, type Renderable } from "../output";
|
|
47
|
+
import { assertFlagAtMostOnce, parseCommandArgs, singleOptionValue, workspaceSelection } from "./args";
|
|
48
|
+
|
|
49
|
+
/** Options for {@link runGraph}; `root` and the streams are injectable for tests. */
|
|
50
|
+
export interface GraphOptions {
|
|
51
|
+
/** The repo root the `docs/` bundle resolves against. */
|
|
52
|
+
root: string;
|
|
53
|
+
/** The resolved output mode/color (from `output.ts`). */
|
|
54
|
+
output: OutputContext;
|
|
55
|
+
/** The command's normalized positional + flag tokens from Commander. */
|
|
56
|
+
args: readonly string[];
|
|
57
|
+
/** stdout sink; defaults to `process.stdout`. */
|
|
58
|
+
stdout?: Writer;
|
|
59
|
+
/** stderr sink for advisory warnings; defaults to `process.stderr`. */
|
|
60
|
+
stderr?: Writer;
|
|
61
|
+
/** Backlog snapshot seam used only by indexed projection freshness/builds. */
|
|
62
|
+
adapter?: BacklogAdapter;
|
|
63
|
+
/** Indexed/reference selector injected by the Commander handler or conformance tests. */
|
|
64
|
+
retrieval?: RetrievalGraphLoader;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The parsed form of `lore graph`'s arguments. */
|
|
68
|
+
interface GraphArgs {
|
|
69
|
+
/** The root concept id (positional, already {@link idFromPath}-normalized); `undefined` exports the whole bundle. */
|
|
70
|
+
id?: string;
|
|
71
|
+
/** Emit Graphviz DOT instead of the node/edge listing (`--dot`). */
|
|
72
|
+
dot: boolean;
|
|
73
|
+
/** The hop radius (`--depth`); `undefined` means unbounded. Requires `id`. */
|
|
74
|
+
depth?: number;
|
|
75
|
+
readonly workspace?: WorkspaceRetrievalSelection;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Run `lore graph`: parse the arguments, load the bundle, narrow to the rooted
|
|
80
|
+
* subgraph when an `<id>` is given, shape the export, emit the `graph.export`,
|
|
81
|
+
* and return `0`. A bad flag/positional (including `--dot` with `--json`) throws
|
|
82
|
+
* a `usage` {@link LoreError} (exit `2`); an `<id>` not in the bundle a
|
|
83
|
+
* `not_found` one (exit `3`).
|
|
84
|
+
*/
|
|
85
|
+
export function runGraph(options: GraphOptions): number | Promise<number> {
|
|
86
|
+
const parsed = parseGraphArgs(options.args);
|
|
87
|
+
if (parsed.dot && options.output.mode === "json") {
|
|
88
|
+
throw usage("--dot cannot be combined with --json", "DOT has no JSON envelope; pass one of --dot or --json");
|
|
89
|
+
}
|
|
90
|
+
const advisories = new WarningCollector();
|
|
91
|
+
const retrieval = options.retrieval ?? (parsed.workspace !== undefined ? loadRetrievalGraph : undefined);
|
|
92
|
+
if (retrieval !== undefined) {
|
|
93
|
+
return retrieval({
|
|
94
|
+
root: options.root,
|
|
95
|
+
warnings: advisories,
|
|
96
|
+
adapter: options.adapter,
|
|
97
|
+
...(parsed.workspace !== undefined ? { workspace: parsed.workspace } : {}),
|
|
98
|
+
}).then(async (loaded) => {
|
|
99
|
+
try {
|
|
100
|
+
return finishGraph(options, parsed, loaded.graph, advisories, loaded.workspace);
|
|
101
|
+
} finally {
|
|
102
|
+
await loaded.dispose?.();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const profile = loadProfile({ root: options.root });
|
|
107
|
+
const graph = loadBundle(join(options.root, DOCS_DIR), { warnings: advisories, profile });
|
|
108
|
+
return finishGraph(options, parsed, graph, advisories);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function finishGraph(
|
|
112
|
+
options: GraphOptions,
|
|
113
|
+
parsed: GraphArgs,
|
|
114
|
+
graph: ReturnType<typeof loadBundle>,
|
|
115
|
+
advisories: WarningCollector,
|
|
116
|
+
workspace?: WorkspaceRetrievalContext,
|
|
117
|
+
): number {
|
|
118
|
+
// Flush load warnings before the subgraph lookup, which throws not_found for an
|
|
119
|
+
// unknown root — otherwise an advisory that explains *why* a file is not a
|
|
120
|
+
// concept would be discarded on exactly the path that most needs it.
|
|
121
|
+
advisories.flush({ color: options.output.color, stderr: options.stderr });
|
|
122
|
+
|
|
123
|
+
let data: GraphExport;
|
|
124
|
+
if (parsed.id === undefined) {
|
|
125
|
+
data = buildGraphExport(graph, workspaceOptions(workspace));
|
|
126
|
+
} else {
|
|
127
|
+
const include = subgraph(graph, parsed.id, parsed.depth ?? Number.POSITIVE_INFINITY);
|
|
128
|
+
data = buildGraphExport(graph, {
|
|
129
|
+
include,
|
|
130
|
+
root: parsed.id,
|
|
131
|
+
depth: parsed.depth,
|
|
132
|
+
...workspaceOptions(workspace, include),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
emit(graphRenderable(data, parsed.dot), options.output, options.stdout);
|
|
137
|
+
return EXIT_OK;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Argument parsing ───────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Parse `graph`'s tokens into the optional root `<id>` positional, the boolean
|
|
144
|
+
* `--dot`, and the value flag `--depth <n>` (also accepting `--depth=<n>`). The
|
|
145
|
+
* Commander has already resolved Lore's global flags, so a `--`-prefixed token here
|
|
146
|
+
* is a command flag: an unrecognized one is a `usage` error, as is a repeated or
|
|
147
|
+
* value-bearing `--dot`, a repeated/value-less/non-integer/too-large `--depth`, a
|
|
148
|
+
* `--depth` with no root, or a second positional. A `--` ends option parsing. The
|
|
149
|
+
* `<id>` is {@link idFromPath}-normalized so path/`.md`/`./` forms resolve.
|
|
150
|
+
*/
|
|
151
|
+
function parseGraphArgs(args: readonly string[]): GraphArgs {
|
|
152
|
+
const parsed = parseCommandArgs(args, "graph");
|
|
153
|
+
const workspace = workspaceSelection(parsed);
|
|
154
|
+
const positionals = parsed.positionals;
|
|
155
|
+
assertFlagAtMostOnce(parsed, "dot");
|
|
156
|
+
const rawDepth = singleOptionValue(parsed, "depth");
|
|
157
|
+
if (rawDepth === "") throw usage("--depth needs a value", "pass a value, e.g. `--depth 2`");
|
|
158
|
+
const depth = rawDepth === undefined ? undefined : parseDepth(rawDepth);
|
|
159
|
+
if (positionals.length > 1) {
|
|
160
|
+
throw usage(`unexpected argument "${positionals[1]}"`, "run `lore graph [<id>] [--dot] [--depth <n>]`");
|
|
161
|
+
}
|
|
162
|
+
const raw = positionals[0];
|
|
163
|
+
if (depth !== undefined && raw === undefined) {
|
|
164
|
+
throw usage(
|
|
165
|
+
"--depth needs a root <id>",
|
|
166
|
+
"give the concept to bound the radius from, e.g. `lore graph <id> --depth 2`",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
id: raw !== undefined ? normalizeGraphId(raw, workspace !== undefined) : undefined,
|
|
171
|
+
dot: parsed.flags.has("dot"),
|
|
172
|
+
depth,
|
|
173
|
+
workspace,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function normalizeGraphId(raw: string, workspace: boolean): string {
|
|
178
|
+
if (!workspace) return idFromPath(raw);
|
|
179
|
+
try {
|
|
180
|
+
const parsed = parseQualifiedWorkspaceId(raw);
|
|
181
|
+
return qualifyWorkspaceId(parsed.memberId, idFromPath(parsed.sourceId));
|
|
182
|
+
} catch {
|
|
183
|
+
throw usage(`invalid workspace concept id "${raw}"`, "use the unambiguous <member-id>::<source-id> form");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function workspaceOptions(workspace?: WorkspaceRetrievalContext, include?: ReadonlySet<string>) {
|
|
188
|
+
if (workspace === undefined) return {};
|
|
189
|
+
const links =
|
|
190
|
+
include === undefined
|
|
191
|
+
? workspace.links
|
|
192
|
+
: workspace.links.filter(
|
|
193
|
+
(link) =>
|
|
194
|
+
include.has(qualifyWorkspaceId(link.from.memberId, link.from.sourceId)) &&
|
|
195
|
+
include.has(qualifyWorkspaceId(link.to.memberId, link.to.sourceId)),
|
|
196
|
+
);
|
|
197
|
+
return {
|
|
198
|
+
workspace: workspace.scope,
|
|
199
|
+
provenanceById: workspace.provenanceById,
|
|
200
|
+
workspaceLinks: links,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Parse a `--depth` value as a non-negative, safe integer (`0` = root only). */
|
|
205
|
+
function parseDepth(value: string): number {
|
|
206
|
+
// Accept only a bare run of digits — Number() would coerce "1.5"/"0x2"/" 2 "/"1e3".
|
|
207
|
+
if (!/^\d+$/.test(value)) {
|
|
208
|
+
throw usage(
|
|
209
|
+
`invalid --depth "${value}"`,
|
|
210
|
+
"pass a non-negative integer, e.g. `--depth 2`; --depth needs a value before a separate flag-looking token",
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const depth = Number.parseInt(value, 10);
|
|
214
|
+
// A >2^53 run of digits parses without error but loses precision, so the echoed
|
|
215
|
+
// `depth` would differ from what the user typed; reject it rather than lie.
|
|
216
|
+
if (!Number.isSafeInteger(depth)) {
|
|
217
|
+
throw usage(`--depth "${value}" is too large`, "pass a smaller non-negative integer");
|
|
218
|
+
}
|
|
219
|
+
return depth;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Read a value flag's argument: its inline `--flag=value` form when present, else
|
|
224
|
+
* the **next** token. A missing/empty value — or a next token that is itself an
|
|
225
|
+
* option (`--depth --dot`) — is a `usage` error rather than a silently swallowed
|
|
226
|
+
* flag (mirroring `lore schema`'s value-flag guard).
|
|
227
|
+
*/
|
|
228
|
+
// ── Output ─────────────────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The per-result rendering bundle for `graph` (output.ts dispatches on the mode).
|
|
232
|
+
* `--json` always carries the structured {@link GraphExport}; the pretty/plain
|
|
233
|
+
* text is the DOT serialization under `--dot`, else a human node/edge listing.
|
|
234
|
+
* The two text modes render identically (the data is structural — no severities
|
|
235
|
+
* to color), so pretty and plain share one renderer.
|
|
236
|
+
*/
|
|
237
|
+
function graphRenderable(data: GraphExport, dot: boolean): Renderable<GraphExport> {
|
|
238
|
+
const render = dot ? toDot : renderText;
|
|
239
|
+
return { kind: "graph.export", data, pretty: render, plain: render };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* A human/pipe-stable listing: a `<n> concepts, <m> edges, ~<t> tokens` header
|
|
244
|
+
* (annotating the scope when it is a subgraph), one `<id> [type] ~<tok>` line
|
|
245
|
+
* per node, then one `<from> -<kind>-> <to>` line per edge (`(dangling: <target>)`
|
|
246
|
+
* for a broken reference). ANSI-free and deterministic.
|
|
247
|
+
*
|
|
248
|
+
* `node.id`/`node.type`/`node.title`/edge endpoints all come from bundle-controlled
|
|
249
|
+
* frontmatter (an edge's `from`/`to` are concept ids; `target` is the reference
|
|
250
|
+
* as parsed, which for a dangling `specs`/frontmatter edge can carry whatever a
|
|
251
|
+
* YAML scalar allows — including an embedded newline; `node.type` mirrors
|
|
252
|
+
* `frontmatter.type`, and `requireType` (schema.ts) only trims the *ends* of the
|
|
253
|
+
* value while unknown types are warn-only, so an interior newline in `type:`
|
|
254
|
+
* survives bundle load unchanged), so each is run through {@link singleLine}
|
|
255
|
+
* before it lands in a line — the same guard every other bundle-text renderer
|
|
256
|
+
* applies (managed-block.ts, indexes.ts, context.ts, query.ts, log.ts) — so an
|
|
257
|
+
* embedded newline/control character cannot split one node or edge into extra
|
|
258
|
+
* physical lines. `data.root` is included for the same reason: though only
|
|
259
|
+
* reachable via a concept id that itself embeds a newline, guarding it keeps
|
|
260
|
+
* the header consistent with every id printed below it.
|
|
261
|
+
*/
|
|
262
|
+
function renderText(data: GraphExport): string {
|
|
263
|
+
const root = data.root !== undefined ? singleLine(data.root) : undefined;
|
|
264
|
+
const scope =
|
|
265
|
+
root !== undefined ? ` rooted at ${root}${data.depth !== undefined ? ` (depth ${data.depth})` : ""}` : "";
|
|
266
|
+
const lines = [
|
|
267
|
+
...(data.workspace !== undefined
|
|
268
|
+
? [`workspace: ${singleLine(data.workspace.workspaceId)} (${data.workspace.repositories.length} repositories)`]
|
|
269
|
+
: []),
|
|
270
|
+
`${data.nodes.length} ${plural(data.nodes.length, "concept")}, ${data.edges.length} ${plural(data.edges.length, "edge")}, ~${data.tokenEstimate} tokens (chars/4)${scope}`,
|
|
271
|
+
];
|
|
272
|
+
for (const node of data.nodes) {
|
|
273
|
+
const title = node.title !== undefined ? ` ${singleLine(node.title)}` : "";
|
|
274
|
+
lines.push(` ${singleLine(node.id)} [${singleLine(node.type)}] ~${node.tokenEstimate}${title}`);
|
|
275
|
+
}
|
|
276
|
+
for (const edge of data.edges) {
|
|
277
|
+
const dest = edge.to !== null ? singleLine(edge.to) : `(dangling: ${singleLine(edge.target)})`;
|
|
278
|
+
lines.push(` ${singleLine(edge.from)} -${edge.kind}-> ${dest}`);
|
|
279
|
+
}
|
|
280
|
+
return lines.join("\n");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Pluralize a noun by count (`1 concept` / `2 concepts`). */
|
|
284
|
+
function plural(count: number, noun: string): string {
|
|
285
|
+
return count === 1 ? noun : `${noun}s`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** A `usage` {@link LoreError} (exit `2`) with an actionable hint. */
|
|
289
|
+
function usage(message: string, hint: string): LoreError {
|
|
290
|
+
return new LoreError("usage", message, hint);
|
|
291
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/help.ts — `lore help [<command>]` (cli-surface §help).
|
|
3
|
+
*
|
|
4
|
+
* The thin, read-only layer over the capability manifest in `core/manifest.ts`:
|
|
5
|
+
* it parses the one optional `<command>` positional and emits either the whole
|
|
6
|
+
* manifest or one command's entry. Like `instructions`, there is no bundle to
|
|
7
|
+
* load and no config to read, so it needs neither `root` nor a `WarningCollector`.
|
|
8
|
+
*
|
|
9
|
+
* - `lore help` — top-level help (the command catalog), rendered from the manifest.
|
|
10
|
+
* - `lore help <command>` — one command's detailed help; an unknown `<command>`
|
|
11
|
+
* is a `not_found` error (exit 3) whose hint lists the valid names.
|
|
12
|
+
* - `--json` — the `{schemaVersion, kind: "help.manifest", data}` envelope, where
|
|
13
|
+
* `data` is the full {@link Manifest} (or, for `lore help <command> --json`, the
|
|
14
|
+
* same {@link Manifest} shape scoped to the one requested command, so a consumer
|
|
15
|
+
* parses both forms identically).
|
|
16
|
+
*
|
|
17
|
+
* {@link renderTopLevelHelp} is exported so the router's `--help`/no-command
|
|
18
|
+
* short-circuit renders from the same manifest — one source for all help text, no
|
|
19
|
+
* separately-maintained `USAGE` literal to drift.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
buildManifest,
|
|
24
|
+
findManifestCommand,
|
|
25
|
+
type Manifest,
|
|
26
|
+
type ManifestCommand,
|
|
27
|
+
type ManifestFlag,
|
|
28
|
+
} from "../core/manifest";
|
|
29
|
+
import { EXIT_OK, LoreError, type Writer } from "../errors";
|
|
30
|
+
import { VERSION } from "../meta";
|
|
31
|
+
import { emit, type OutputContext, type Renderable } from "../output";
|
|
32
|
+
import { parseCommandArgs, usage } from "./args";
|
|
33
|
+
|
|
34
|
+
/** Options for {@link runHelp}; the stream is injectable for tests. */
|
|
35
|
+
export interface HelpOptions {
|
|
36
|
+
/** The resolved output mode/color (from `output.ts`). */
|
|
37
|
+
output: OutputContext;
|
|
38
|
+
/** The command's normalized positional tokens from Commander. */
|
|
39
|
+
args: readonly string[];
|
|
40
|
+
/** stdout sink; defaults to `process.stdout`. */
|
|
41
|
+
stdout?: Writer;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Run `lore help`: parse the optional `<command>`, resolve it, emit its (or the whole) manifest, and return `0`. */
|
|
45
|
+
export function runHelp(options: HelpOptions): number {
|
|
46
|
+
const name = parseHelpArgs(options.args);
|
|
47
|
+
const manifest = buildManifest();
|
|
48
|
+
if (name !== undefined) {
|
|
49
|
+
const command = findManifestCommand(name);
|
|
50
|
+
if (command === undefined) {
|
|
51
|
+
const validNames = manifest.commands.map((c) => c.name).join(", ");
|
|
52
|
+
throw new LoreError(
|
|
53
|
+
"not_found",
|
|
54
|
+
`unknown command "${name}"`,
|
|
55
|
+
`valid commands: ${validNames}; run \`lore help\` to list them`,
|
|
56
|
+
{ command: name },
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
emit(commandHelpRenderable(manifest, command), options.output, options.stdout);
|
|
60
|
+
return EXIT_OK;
|
|
61
|
+
}
|
|
62
|
+
emit(topLevelHelpRenderable(manifest), options.output, options.stdout);
|
|
63
|
+
return EXIT_OK;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Parse `help`'s tokens via the shared parser (no known flags): at most one positional (the command name); a flag or second positional is a `usage` error. */
|
|
67
|
+
function parseHelpArgs(args: readonly string[]): string | undefined {
|
|
68
|
+
const { positionals } = parseCommandArgs(args, "help");
|
|
69
|
+
if (positionals.length > 1) {
|
|
70
|
+
throw usage(`unexpected argument "${positionals[1]}"`, "run `lore help [<command>]`");
|
|
71
|
+
}
|
|
72
|
+
return positionals[0];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The whole-manifest {@link Renderable}: `data` is the full manifest; pretty/plain are the top-level command catalog. */
|
|
76
|
+
function topLevelHelpRenderable(manifest: Manifest): Renderable<Manifest> {
|
|
77
|
+
const text = renderTopLevelHelp(manifest);
|
|
78
|
+
return { kind: "help.manifest", data: manifest, pretty: () => text, plain: () => text };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** One command's {@link Renderable}: `data` is the manifest scoped to that command (same shape as the full one); pretty/plain are its detailed help. */
|
|
82
|
+
function commandHelpRenderable(manifest: Manifest, command: ManifestCommand): Renderable<Manifest> {
|
|
83
|
+
const text = renderCommandHelp(command);
|
|
84
|
+
const data: Manifest = { ...manifest, commands: [command] };
|
|
85
|
+
return { kind: "help.manifest", data, pretty: () => text, plain: () => text };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Render the top-level help — the header, the command catalog, and the global
|
|
90
|
+
* options — from the manifest. Uncolored, so the `lore help` command and the
|
|
91
|
+
* `lore --help` flag (which the router feeds this same text) are byte-identical.
|
|
92
|
+
* Exported for the router's `--help`/no-command short-circuit.
|
|
93
|
+
*/
|
|
94
|
+
export function renderTopLevelHelp(manifest: Manifest = buildManifest()): string {
|
|
95
|
+
const nameWidth = Math.max(...manifest.commands.map((c) => c.name.length));
|
|
96
|
+
const commands = manifest.commands
|
|
97
|
+
.map((c) => ` ${c.name.padEnd(nameWidth)} ${c.summary}${c.args ? ` (lore ${c.name} ${c.args})` : ""}`)
|
|
98
|
+
.join("\n");
|
|
99
|
+
const optWidth = Math.max(...manifest.globalFlags.map((f) => globalFlagLabel(f).length));
|
|
100
|
+
const options = manifest.globalFlags.map((f) => ` ${globalFlagLabel(f).padEnd(optWidth)} ${f.summary}`).join("\n");
|
|
101
|
+
return `lore ${VERSION} — OKF-native documentation CLI
|
|
102
|
+
|
|
103
|
+
Usage:
|
|
104
|
+
lore <command> [options]
|
|
105
|
+
|
|
106
|
+
Commands:
|
|
107
|
+
${commands}
|
|
108
|
+
|
|
109
|
+
Run \`lore help <command>\` for a command's arguments, flags, output, and exit codes.
|
|
110
|
+
|
|
111
|
+
Options:
|
|
112
|
+
${options}
|
|
113
|
+
|
|
114
|
+
Docs: docs/index.md`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Render one command's detailed help: usage line, flags, output kind, exit codes, and examples. */
|
|
118
|
+
function renderCommandHelp(command: ManifestCommand): string {
|
|
119
|
+
const lines: string[] = [
|
|
120
|
+
`lore ${command.name} — ${command.summary}`,
|
|
121
|
+
"",
|
|
122
|
+
"Usage:",
|
|
123
|
+
` lore ${command.name}${command.args ? ` ${command.args}` : ""}`,
|
|
124
|
+
];
|
|
125
|
+
if (command.flags.length > 0) {
|
|
126
|
+
const width = Math.max(...command.flags.map((f) => commandFlagLabel(f).length));
|
|
127
|
+
lines.push("", "Flags:");
|
|
128
|
+
for (const flag of command.flags) {
|
|
129
|
+
const note = flag.repeatable ? " (repeatable)" : "";
|
|
130
|
+
lines.push(` ${commandFlagLabel(flag).padEnd(width)} ${flag.summary}${note}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
lines.push("", `Output: ${command.kind} (--json envelope)`, `Exit codes: ${command.exitCodes.join(", ")}`);
|
|
134
|
+
if (command.examples.length > 0) {
|
|
135
|
+
lines.push("", "Examples:");
|
|
136
|
+
for (const example of command.examples) {
|
|
137
|
+
lines.push(` ${example}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return lines.join("\n");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** `-v, --version` when a flag has a short alias, else `--json`. */
|
|
144
|
+
function globalFlagLabel(flag: ManifestFlag): string {
|
|
145
|
+
return flag.alias !== undefined ? `-${flag.alias}, --${flag.name}` : `--${flag.name}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** `--type <value>` for a value-taking flag, else `--strict`. */
|
|
149
|
+
function commandFlagLabel(flag: ManifestFlag): string {
|
|
150
|
+
return flag.takesValue ? `--${flag.name} <value>` : `--${flag.name}`;
|
|
151
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** `lore impact`: bounded deterministic impact expansion across exact authored typed edges. */
|
|
2
|
+
|
|
3
|
+
import type { BacklogAdapter } from "../adapters/backlog";
|
|
4
|
+
import { loadRetrievalGraph, type RetrievalGraphLoader } from "../core/retrieval";
|
|
5
|
+
import { findImpact } from "../core/traversal";
|
|
6
|
+
import { EXIT_OK, LoreError, WarningCollector, type Writer } from "../errors";
|
|
7
|
+
import { emit, type OutputContext } from "../output";
|
|
8
|
+
import { parseCommandArgs, usage, workspaceSelection } from "./args";
|
|
9
|
+
import {
|
|
10
|
+
assertKnownEdgeKinds,
|
|
11
|
+
impactRenderable,
|
|
12
|
+
normalizeEndpointId,
|
|
13
|
+
parseEndpointKind,
|
|
14
|
+
parseTraversalFlags,
|
|
15
|
+
} from "./traversal";
|
|
16
|
+
|
|
17
|
+
export interface ImpactCommandOptions {
|
|
18
|
+
readonly root: string;
|
|
19
|
+
readonly output: OutputContext;
|
|
20
|
+
readonly args: readonly string[];
|
|
21
|
+
readonly stdout?: Writer;
|
|
22
|
+
readonly stderr?: Writer;
|
|
23
|
+
readonly adapter?: BacklogAdapter;
|
|
24
|
+
readonly retrieval?: RetrievalGraphLoader;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runImpact(options: ImpactCommandOptions): Promise<number> {
|
|
28
|
+
const parsed = parseCommandArgs(options.args, "impact");
|
|
29
|
+
const workspace = workspaceSelection(parsed);
|
|
30
|
+
if (parsed.positionals.length !== 1) {
|
|
31
|
+
throw usage("impact needs exactly one <id>", "run `lore impact <id> --kind <kind> --direction <direction>`");
|
|
32
|
+
}
|
|
33
|
+
const kind = parseEndpointKind(parsed, "kind");
|
|
34
|
+
const flags = parseTraversalFlags(parsed);
|
|
35
|
+
const advisories = new WarningCollector();
|
|
36
|
+
const loaded = await (options.retrieval ?? loadRetrievalGraph)({
|
|
37
|
+
root: options.root,
|
|
38
|
+
warnings: advisories,
|
|
39
|
+
adapter: options.adapter,
|
|
40
|
+
includeTraversal: true,
|
|
41
|
+
...(workspace !== undefined ? { workspace } : {}),
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
advisories.flush({ color: options.output.color, stderr: options.stderr });
|
|
45
|
+
if (loaded.traversal === undefined) throw new LoreError("validation", "traversal snapshot was not loaded");
|
|
46
|
+
assertKnownEdgeKinds(loaded.traversal, flags.edgeKinds);
|
|
47
|
+
const data = findImpact(loaded.traversal, {
|
|
48
|
+
root: {
|
|
49
|
+
kind,
|
|
50
|
+
id: normalizeEndpointId(parsed.positionals[0] as string, kind, workspace !== undefined),
|
|
51
|
+
},
|
|
52
|
+
...flags,
|
|
53
|
+
});
|
|
54
|
+
emit(impactRenderable(data), options.output, options.stdout);
|
|
55
|
+
return EXIT_OK;
|
|
56
|
+
} finally {
|
|
57
|
+
await loaded.dispose?.();
|
|
58
|
+
}
|
|
59
|
+
}
|