@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,49 +0,0 @@
|
|
|
1
|
-
import { dirname, join } from "node:path";
|
|
2
|
-
import type { BridgeAction } from "../core/agent-bridge";
|
|
3
|
-
import { AGENTS_MD_REL_PATH, CODEX_SKILL_REL_PATH, planCodexBridge } from "../core/codex-bridge";
|
|
4
|
-
import { readFileIfPresent } from "../errors";
|
|
5
|
-
import { assertNoSymlinkInAnyPath, ensureDir, writeFileAtomic } from "./fswrite";
|
|
6
|
-
|
|
7
|
-
export interface CodexBridgeResult {
|
|
8
|
-
root: string;
|
|
9
|
-
files: ReadonlyArray<{ path: string; action: BridgeAction }>;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function applyCodexBridge(options: { root: string; force: boolean; check: boolean }): CodexBridgeResult {
|
|
13
|
-
const skillRaw = readFileIfPresent(join(options.root, CODEX_SKILL_REL_PATH), CODEX_SKILL_REL_PATH);
|
|
14
|
-
const agentsRaw = readFileIfPresent(join(options.root, AGENTS_MD_REL_PATH), AGENTS_MD_REL_PATH);
|
|
15
|
-
const skillOnDisk = normalize(skillRaw);
|
|
16
|
-
const agentsOnDisk = normalize(agentsRaw);
|
|
17
|
-
const agentsStyle = detectStyle(agentsRaw);
|
|
18
|
-
const plan = planCodexBridge({ skillOnDisk, agentsOnDisk, force: options.force, check: options.check });
|
|
19
|
-
|
|
20
|
-
if (!options.check) {
|
|
21
|
-
const targets = plan.files.filter((file) => file.contents !== null).map((file) => file.path);
|
|
22
|
-
assertNoSymlinkInAnyPath(options.root, targets);
|
|
23
|
-
for (const file of plan.files) {
|
|
24
|
-
if (file.contents === null) continue;
|
|
25
|
-
ensureDir(options.root, dirname(file.path));
|
|
26
|
-
const contents = file.path === AGENTS_MD_REL_PATH ? restyle(file.contents, agentsStyle) : file.contents;
|
|
27
|
-
writeFileAtomic(join(options.root, file.path), contents, file.path);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return { root: options.root, files: plan.files.map(({ path, action }) => ({ path, action })) };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function normalize(raw: string | undefined): string | null {
|
|
35
|
-
return raw === undefined ? null : raw.replace(/^\uFEFF+/, "").replace(/\r\n?/g, "\n");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function detectStyle(raw: string | undefined): { bom: boolean; eol: "\n" | "\r\n" | "\r" } {
|
|
39
|
-
if (raw === undefined) return { bom: false, eol: "\n" };
|
|
40
|
-
return {
|
|
41
|
-
bom: raw.startsWith("\uFEFF"),
|
|
42
|
-
eol: raw.includes("\r\n") ? "\r\n" : raw.includes("\r") ? "\r" : "\n",
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function restyle(contents: string, style: { bom: boolean; eol: "\n" | "\r\n" | "\r" }): string {
|
|
47
|
-
const withEol = style.eol === "\n" ? contents : contents.replace(/\n/g, style.eol);
|
|
48
|
-
return style.bom ? `\uFEFF${withEol}` : withEol;
|
|
49
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* commands/concurrency.ts — {@link mapWithConcurrency}, a tiny worker-pool over a shared cursor,
|
|
3
|
-
* plus {@link TASK_DETAILS_CONCURRENCY}, the shared cap it bounds Backlog `viewTask` fan-out to.
|
|
4
|
-
*
|
|
5
|
-
* Pure and dependency-free (no import of any other `commands/` module) so it can be shared by
|
|
6
|
-
* `commands/link.ts` and `commands/reconcile-shared.ts` without either importing the other:
|
|
7
|
-
* `reconcile-shared.ts` already imports `verifiedViewTask`/`dedupeTaskIds`/`defaultAdapter` FROM
|
|
8
|
-
* `link.ts`, so `link.ts` importing anything back from `reconcile-shared.ts` would create a
|
|
9
|
-
* `link -> reconcile-shared -> link` cycle (LORE-233). Originally lived solely in
|
|
10
|
-
* `reconcile-shared.ts` (LORE-111, bounding `resolveTaskDetails`'s fan-out and `check.ts`'s
|
|
11
|
-
* `probeLiveness`); relocated here so `link.ts`'s own up-front task-existence-check fan-out
|
|
12
|
-
* (`runLink`) could reuse the identical helper and cap instead of duplicating either.
|
|
13
|
-
* `reconcile-shared.ts` re-exports both so its own existing importers (`check.ts`,
|
|
14
|
-
* `reconcile-shared.test.ts`) keep working unchanged.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* How many `adapter.viewTask` calls a caller runs at once — bounded so a large task-id fan-out
|
|
19
|
-
* (a bundle linking many distinct ids, or a multi-id `lore link`) does not spawn one Backlog CLI
|
|
20
|
-
* subprocess per id fully concurrently (which can exhaust process/file-descriptor limits or
|
|
21
|
-
* overwhelm the Backlog CLI). Mirrors `check.ts`'s own `LIVENESS_CONCURRENCY` cap on external-URL
|
|
22
|
-
* liveness probes — same shape of problem (unbounded subprocess/socket fan-out), same fix.
|
|
23
|
-
* Exported so tests can assert against the real cap rather than duplicating (and risking drift
|
|
24
|
-
* from) a hardcoded copy.
|
|
25
|
-
*/
|
|
26
|
-
export const TASK_DETAILS_CONCURRENCY = 8;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Run `fn` over `items` with at most `limit` in flight at once — a tiny worker-pool over a shared
|
|
30
|
-
* cursor. Shared by `reconcile-shared.ts`'s `resolveTaskDetails` (bounding concurrent
|
|
31
|
-
* `adapter.viewTask` Backlog subprocess spawns), `check.ts`'s `probeLiveness` (bounding concurrent
|
|
32
|
-
* external-URL fetches), and `link.ts`'s `runLink` (bounding its own up-front `verifiedViewTask`
|
|
33
|
-
* existence-check fan-out).
|
|
34
|
-
*/
|
|
35
|
-
export async function mapWithConcurrency<T>(
|
|
36
|
-
items: readonly T[],
|
|
37
|
-
limit: number,
|
|
38
|
-
fn: (item: T) => Promise<void>,
|
|
39
|
-
): Promise<void> {
|
|
40
|
-
let cursor = 0;
|
|
41
|
-
const worker = async (): Promise<void> => {
|
|
42
|
-
while (cursor < items.length) {
|
|
43
|
-
const item = items[cursor++] as T;
|
|
44
|
-
await fn(item);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
48
|
-
}
|
package/src/commands/context.ts
DELETED
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* commands/context.ts — `lore context <id> [--max-tokens <n>] [--depth <n>]`.
|
|
3
|
-
*
|
|
4
|
-
* The thin, read-only layer that emits a **token-budgeted context pack** for one
|
|
5
|
-
* concept (cli-surface §context; LORE-34). Commander supplies a verified indexed
|
|
6
|
-
* {@link BundleGraph} with automatic reference fallback; direct core callers
|
|
7
|
-
* retain the reference loader. It then hands the target id, neighbor radius
|
|
8
|
-
* (`--depth`, default {@link DEFAULT_DEPTH}), and budget (`--max-tokens`) to the
|
|
9
|
-
* pure {@link buildContext} shaper, which gathers the target's neighborhood via
|
|
10
|
-
* the shared {@link subgraph} traversal and compacts it to the target's full body
|
|
11
|
-
* plus one-line neighbor summaries.
|
|
12
|
-
*
|
|
13
|
-
* Output follows the uniform CLI modes: the `{schemaVersion, kind:
|
|
14
|
-
* "context.export", data}` envelope under the global `--json`, and otherwise a
|
|
15
|
-
* pasteable text pack — the target's body followed by the neighbor compaction —
|
|
16
|
-
* with a §3 truncation footer when the budget dropped neighbors. There is no
|
|
17
|
-
* command-specific JSON flag; machine consumers use the same `--json` they use
|
|
18
|
-
* everywhere. The pretty and plain renderers are identical (the pack is structural —
|
|
19
|
-
* no severities to color), so they share one renderer.
|
|
20
|
-
*
|
|
21
|
-
* The positional `<id>` is normalized through {@link idFromPath} exactly as `lore
|
|
22
|
-
* graph`/`rename`/`supersede` normalize theirs, so a path-form, `./`-prefixed, or
|
|
23
|
-
* `.md`-suffixed id resolves to the same bundle key.
|
|
24
|
-
*
|
|
25
|
-
* Validation lives here (the budget/compaction computation stays pure in
|
|
26
|
-
* `core/context.ts`): a missing or duplicate `<id>`, an unknown flag, a
|
|
27
|
-
* repeated/value-less/non-integer/too-large/non-positive `--max-tokens`, or a
|
|
28
|
-
* repeated/value-less/non-integer/too-large/negative `--depth` is a `usage` error
|
|
29
|
-
* (exit 2); an `<id>` absent from the bundle surfaces as the `not_found` error
|
|
30
|
-
* (exit 3) {@link buildContext} throws.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
import { join } from "node:path";
|
|
34
|
-
import type { BacklogAdapter } from "../adapters/backlog";
|
|
35
|
-
import { loadBundle } from "../core/bundle";
|
|
36
|
-
import { idFromPath } from "../core/concept";
|
|
37
|
-
import { buildContext, type ContextExport, DEFAULT_DEPTH } from "../core/context";
|
|
38
|
-
import { loadProfile } from "../core/profile";
|
|
39
|
-
import { loadRetrievalGraph, type RetrievalGraphLoader } from "../core/retrieval";
|
|
40
|
-
import { DOCS_DIR } from "../core/scaffold";
|
|
41
|
-
import { parseQualifiedWorkspaceId, qualifyWorkspaceId } from "../core/workspace-contract";
|
|
42
|
-
import type { WorkspaceRetrievalContext, WorkspaceRetrievalSelection } from "../core/workspace-retrieval";
|
|
43
|
-
import { EXIT_OK, LoreError, WarningCollector, type Writer } from "../errors";
|
|
44
|
-
import { emit, type OutputContext, type Renderable, renderTruncationLine, truncation } from "../output";
|
|
45
|
-
import { parseCommandArgs, singleOptionValue, workspaceSelection } from "./args";
|
|
46
|
-
|
|
47
|
-
/** Options for {@link runContext}; `root` and the streams are injectable for tests. */
|
|
48
|
-
export interface ContextOptions {
|
|
49
|
-
/** The repo root the `docs/` bundle resolves against. */
|
|
50
|
-
root: string;
|
|
51
|
-
/** The resolved output mode/color (from `output.ts`). */
|
|
52
|
-
output: OutputContext;
|
|
53
|
-
/** The command's normalized positional + flag tokens from Commander. */
|
|
54
|
-
args: readonly string[];
|
|
55
|
-
/** stdout sink; defaults to `process.stdout`. */
|
|
56
|
-
stdout?: Writer;
|
|
57
|
-
/** stderr sink for advisory warnings; defaults to `process.stderr`. */
|
|
58
|
-
stderr?: Writer;
|
|
59
|
-
/** Backlog snapshot seam used only by indexed projection freshness/builds. */
|
|
60
|
-
adapter?: BacklogAdapter;
|
|
61
|
-
/** Indexed/reference selector injected by the Commander handler or conformance tests. */
|
|
62
|
-
retrieval?: RetrievalGraphLoader;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** The parsed form of `lore context`'s arguments. */
|
|
66
|
-
interface ContextArgs {
|
|
67
|
-
/** The target concept id (positional, already {@link idFromPath}-normalized). */
|
|
68
|
-
id: string;
|
|
69
|
-
/** The token budget (`--max-tokens`); `undefined` means no size trim (bounded only by depth). */
|
|
70
|
-
maxTokens?: number;
|
|
71
|
-
/** The hop radius (`--depth`); `undefined` falls back to {@link DEFAULT_DEPTH}. */
|
|
72
|
-
depth?: number;
|
|
73
|
-
readonly workspace?: WorkspaceRetrievalSelection;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Run `lore context`: parse the arguments, load the bundle, build the context pack,
|
|
78
|
-
* emit the `context.export`, and return `0`. A bad flag/positional throws a `usage`
|
|
79
|
-
* {@link LoreError} (exit `2`); an `<id>` not in the bundle a `not_found` one (exit
|
|
80
|
-
* `3`).
|
|
81
|
-
*/
|
|
82
|
-
export function runContext(options: ContextOptions): number | Promise<number> {
|
|
83
|
-
const parsed = parseContextArgs(options.args);
|
|
84
|
-
const advisories = new WarningCollector();
|
|
85
|
-
const retrieval = options.retrieval ?? (parsed.workspace !== undefined ? loadRetrievalGraph : undefined);
|
|
86
|
-
if (retrieval !== undefined) {
|
|
87
|
-
return retrieval({
|
|
88
|
-
root: options.root,
|
|
89
|
-
warnings: advisories,
|
|
90
|
-
adapter: options.adapter,
|
|
91
|
-
...(parsed.workspace !== undefined ? { workspace: parsed.workspace } : {}),
|
|
92
|
-
}).then(async (loaded) => {
|
|
93
|
-
try {
|
|
94
|
-
const graph =
|
|
95
|
-
loaded.indexed === undefined
|
|
96
|
-
? loaded.graph
|
|
97
|
-
: withConceptBody(loaded.graph, parsed.id, await loaded.indexed.readConceptBody(parsed.id));
|
|
98
|
-
return finishContext(options, parsed, graph, advisories, loaded.workspace);
|
|
99
|
-
} finally {
|
|
100
|
-
await loaded.dispose?.();
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
const profile = loadProfile({ root: options.root });
|
|
105
|
-
const graph = loadBundle(join(options.root, DOCS_DIR), { warnings: advisories, profile });
|
|
106
|
-
return finishContext(options, parsed, graph, advisories);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function withConceptBody(
|
|
110
|
-
graph: ReturnType<typeof loadBundle>,
|
|
111
|
-
id: string,
|
|
112
|
-
body: string | undefined,
|
|
113
|
-
): ReturnType<typeof loadBundle> {
|
|
114
|
-
if (body === undefined) return graph;
|
|
115
|
-
const concept = graph.concepts.get(id);
|
|
116
|
-
if (concept === undefined) return graph;
|
|
117
|
-
const concepts = new Map(graph.concepts);
|
|
118
|
-
concepts.set(id, { ...concept, body });
|
|
119
|
-
return { ...graph, concepts };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function finishContext(
|
|
123
|
-
options: ContextOptions,
|
|
124
|
-
parsed: ContextArgs,
|
|
125
|
-
graph: ReturnType<typeof loadBundle>,
|
|
126
|
-
advisories: WarningCollector,
|
|
127
|
-
workspace?: WorkspaceRetrievalContext,
|
|
128
|
-
): number {
|
|
129
|
-
// Flush load warnings before buildContext, which throws not_found for an unknown
|
|
130
|
-
// target — otherwise an advisory explaining *why* a file is not a concept would be
|
|
131
|
-
// discarded on exactly the path that most needs it (mirrors `lore graph`).
|
|
132
|
-
advisories.flush({ color: options.output.color, stderr: options.stderr });
|
|
133
|
-
|
|
134
|
-
const base = buildContext(graph, parsed.id, { depth: parsed.depth, maxTokens: parsed.maxTokens });
|
|
135
|
-
const data: ContextExport =
|
|
136
|
-
workspace === undefined
|
|
137
|
-
? base
|
|
138
|
-
: {
|
|
139
|
-
...base,
|
|
140
|
-
target: { ...base.target, provenance: workspace.provenanceById.get(base.target.id) },
|
|
141
|
-
neighbors: base.neighbors.map((neighbor) => ({
|
|
142
|
-
...neighbor,
|
|
143
|
-
provenance: workspace.provenanceById.get(neighbor.id),
|
|
144
|
-
})),
|
|
145
|
-
workspace: workspace.scope,
|
|
146
|
-
};
|
|
147
|
-
emit(contextRenderable(data), options.output, options.stdout);
|
|
148
|
-
return EXIT_OK;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ── Argument parsing ───────────────────────────────────────────────────────────
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Parse `context`'s tokens into the required `<id>` positional and the value flags
|
|
155
|
-
* `--max-tokens <n>` / `--depth <n>` (also accepting the `--flag=value` form). The
|
|
156
|
-
* Commander has already resolved Lore's global flags, so a `--`-prefixed token here is
|
|
157
|
-
* a command flag: an unrecognized one is a `usage` error, as is a repeated or
|
|
158
|
-
* value-less value flag, a non-integer/out-of-range value, a missing `<id>`, or a
|
|
159
|
-
* second positional. A `--` ends option parsing. The `<id>` is
|
|
160
|
-
* {@link idFromPath}-normalized so path/`.md`/`./` forms resolve.
|
|
161
|
-
*/
|
|
162
|
-
function parseContextArgs(args: readonly string[]): ContextArgs {
|
|
163
|
-
const parsed = parseCommandArgs(args, "context");
|
|
164
|
-
const workspace = workspaceSelection(parsed);
|
|
165
|
-
const positionals = parsed.positionals;
|
|
166
|
-
const rawMaxTokens = singleOptionValue(parsed, "max-tokens");
|
|
167
|
-
const rawDepth = singleOptionValue(parsed, "depth");
|
|
168
|
-
if (rawMaxTokens === "") throw usage("--max-tokens needs a value", "pass a value, e.g. `--max-tokens 2`");
|
|
169
|
-
if (rawDepth === "") throw usage("--depth needs a value", "pass a value, e.g. `--depth 2`");
|
|
170
|
-
const maxTokens = rawMaxTokens === undefined ? undefined : parseCount("--max-tokens", rawMaxTokens, { min: 1 });
|
|
171
|
-
const depth = rawDepth === undefined ? undefined : parseCount("--depth", rawDepth, { min: 0 });
|
|
172
|
-
if (positionals.length === 0) {
|
|
173
|
-
throw usage(
|
|
174
|
-
"`lore context` needs a concept id",
|
|
175
|
-
"give the concept to build context for, e.g. `lore context stories/x`",
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
if (positionals.length > 1) {
|
|
179
|
-
throw usage(`unexpected argument "${positionals[1]}"`, "run `lore context <id> [--max-tokens <n>] [--depth <n>]`");
|
|
180
|
-
}
|
|
181
|
-
return {
|
|
182
|
-
id: normalizeContextId(positionals[0] as string, workspace !== undefined),
|
|
183
|
-
maxTokens,
|
|
184
|
-
depth,
|
|
185
|
-
workspace,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function normalizeContextId(raw: string, workspace: boolean): string {
|
|
190
|
-
if (!workspace) return idFromPath(raw);
|
|
191
|
-
try {
|
|
192
|
-
const parsed = parseQualifiedWorkspaceId(raw);
|
|
193
|
-
return qualifyWorkspaceId(parsed.memberId, idFromPath(parsed.sourceId));
|
|
194
|
-
} catch {
|
|
195
|
-
throw usage(`invalid workspace concept id "${raw}"`, "use the unambiguous <member-id>::<source-id> form");
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Parse a count flag's value as an integer at or above `min` (`--depth` allows `0`,
|
|
201
|
-
* `--max-tokens` requires a positive budget). Rejects a non-digit run
|
|
202
|
-
* (`Number()` would coerce `"1.5"`/`"0x2"`/`" 2 "`/`"1e3"`), a value below `min`,
|
|
203
|
-
* and a precision-losing `> 2^53` run — mirroring `lore graph`'s `--depth` guard so
|
|
204
|
-
* the two commands accept counts identically.
|
|
205
|
-
*/
|
|
206
|
-
function parseCount(flag: string, value: string, opts: { min: number }): number {
|
|
207
|
-
if (!/^\d+$/.test(value)) {
|
|
208
|
-
throw usage(
|
|
209
|
-
`invalid ${flag} "${value}"`,
|
|
210
|
-
`pass an integer ≥ ${opts.min}, e.g. \`${flag} ${opts.min + 1}\`; ${flag} needs a value before a separate flag-looking token`,
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
const count = Number.parseInt(value, 10);
|
|
214
|
-
if (!Number.isSafeInteger(count)) {
|
|
215
|
-
throw usage(`${flag} "${value}" is too large`, "pass a smaller integer");
|
|
216
|
-
}
|
|
217
|
-
if (count < opts.min) {
|
|
218
|
-
throw usage(`invalid ${flag} "${value}"`, `pass an integer ≥ ${opts.min}, e.g. \`${flag} ${opts.min + 1}\``);
|
|
219
|
-
}
|
|
220
|
-
return count;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Read a value flag's argument: its inline `--flag=value` form when present, else
|
|
225
|
-
* the **next** token. A missing/empty value — or a next token that is itself an
|
|
226
|
-
* option (`--depth --max-tokens`) — is a `usage` error rather than a silently
|
|
227
|
-
* swallowed flag (mirroring `lore graph`'s value-flag guard).
|
|
228
|
-
*/
|
|
229
|
-
// ── Output ─────────────────────────────────────────────────────────────────────
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* The per-result rendering bundle for `context` (output.ts dispatches on the mode).
|
|
233
|
-
* `--json` always carries the structured {@link ContextExport}; the pretty/plain
|
|
234
|
-
* text is the pasteable pack. The two text modes render identically (the data is
|
|
235
|
-
* structural — no severities to color), so pretty and plain share one renderer.
|
|
236
|
-
*/
|
|
237
|
-
function contextRenderable(data: ContextExport): Renderable<ContextExport> {
|
|
238
|
-
return { kind: "context.export", data, pretty: renderText, plain: renderText };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* A human/pipe-stable context pack: a header naming the target, its depth/budget and
|
|
243
|
-
* the pack's `~tokens`, then the target's full body, then a `neighbors (<shown> of
|
|
244
|
-
* <total>)` section with one `- <id> [<type>] — <summary>` line each (the `— …`
|
|
245
|
-
* dropped when a neighbor has no summary), and a trailing budget line: an
|
|
246
|
-
* over-budget warning when the always-included target alone exceeds `--max-tokens`,
|
|
247
|
-
* else the §3 truncation footer when the budget dropped neighbors. ANSI-free and
|
|
248
|
-
* deterministic.
|
|
249
|
-
*/
|
|
250
|
-
function renderText(data: ContextExport): string {
|
|
251
|
-
const budget = data.maxTokens !== undefined ? `, budget ${data.maxTokens}` : "";
|
|
252
|
-
const lines = [
|
|
253
|
-
...(data.workspace !== undefined
|
|
254
|
-
? [`workspace: ${data.workspace.workspaceId} (${data.workspace.repositories.length} repositories)`]
|
|
255
|
-
: []),
|
|
256
|
-
`context: ${data.root} [${data.target.type}] — depth ${data.depth}${budget}, ~${data.tokenEstimate} tokens (chars/4)`,
|
|
257
|
-
"",
|
|
258
|
-
data.target.body.replace(/\n+$/, ""),
|
|
259
|
-
"",
|
|
260
|
-
`neighbors (${data.shown} of ${data.total}):`,
|
|
261
|
-
];
|
|
262
|
-
for (const neighbor of data.neighbors) {
|
|
263
|
-
const summary = neighbor.summary !== undefined ? ` — ${neighbor.summary}` : "";
|
|
264
|
-
lines.push(` - ${neighbor.id} [${neighbor.type}]${summary}`);
|
|
265
|
-
}
|
|
266
|
-
const footer = budgetFooter(data);
|
|
267
|
-
if (footer !== "") {
|
|
268
|
-
lines.push(footer);
|
|
269
|
-
}
|
|
270
|
-
return lines.join("\n");
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* The trailing budget line for the pack, or `""` when it fully fit. Over budget (the
|
|
275
|
-
* mandatory target alone exceeds `--max-tokens`, so no neighbor could be dropped to
|
|
276
|
-
* help) gets an explicit warning; otherwise a dropped-neighbor count gets the §3
|
|
277
|
-
* truncation footer. The hint is **only** `raise --max-tokens` — lowering `--depth`
|
|
278
|
-
* cannot surface more neighbors (the included set is a budget-bound nearest-first
|
|
279
|
-
* prefix, so a smaller `--depth` only removes farther candidates that were never
|
|
280
|
-
* going to be included).
|
|
281
|
-
*/
|
|
282
|
-
function budgetFooter(data: ContextExport): string {
|
|
283
|
-
if (data.maxTokens !== undefined && data.tokenEstimate > data.maxTokens) {
|
|
284
|
-
return `over budget: ~${data.tokenEstimate} tokens exceeds the ${data.maxTokens}-token limit — the target is always included; raise --max-tokens`;
|
|
285
|
-
}
|
|
286
|
-
return renderTruncationLine(truncation(data.total, data.shown, "raise --max-tokens to include more"));
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/** A `usage` {@link LoreError} (exit `2`) with an actionable hint. */
|
|
290
|
-
function usage(message: string, hint: string): LoreError {
|
|
291
|
-
return new LoreError("usage", message, hint);
|
|
292
|
-
}
|
package/src/commands/discover.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* commands/discover.ts — the shared file-discovery + read seam for the command layer.
|
|
3
|
-
*
|
|
4
|
-
* Every command that operates over a set of bundle files — `validate`, `check`, `replace` (and the
|
|
5
|
-
* `rename`/`supersede` consumers next) — needs the same three primitives: read a file as UTF-8 with
|
|
6
|
-
* the project's `denied`/`not_found` error classification, derive a stable **canonical identity** for
|
|
7
|
-
* de-duplication, and render a file's **repo-relative** display path. They lived as near-identical
|
|
8
|
-
* copies in each command (a `/code-review max` finding); centralizing them here keeps one behavior, so
|
|
9
|
-
* two commands can't classify a read failure or fold a duplicate path differently. Pairs with
|
|
10
|
-
* {@link fswrite} (the write seam) — discovery/read here, write there, judgement in `core/`.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { readFileSync, realpathSync } from "node:fs";
|
|
14
|
-
import { isAbsolute, join, posix, relative, sep } from "node:path";
|
|
15
|
-
import { walkMarkdown } from "../core/bundle";
|
|
16
|
-
import { DOCS_DIR } from "../core/scaffold";
|
|
17
|
-
import { ioError } from "../errors";
|
|
18
|
-
|
|
19
|
-
/** Read one file as UTF-8, mapping an I/O failure to a classified {@link LoreError} via the shared {@link ioError} policy. */
|
|
20
|
-
export function readSource(abs: string, display: string): string {
|
|
21
|
-
try {
|
|
22
|
-
return readFileSync(abs, "utf8");
|
|
23
|
-
} catch (cause) {
|
|
24
|
-
ioError(cause, {
|
|
25
|
-
denied: { message: `permission denied reading ${display}`, hint: `make ${display} readable` },
|
|
26
|
-
notFound: { message: `cannot read ${display}`, hint: "check the path exists and is readable" },
|
|
27
|
-
input: { path: display },
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** The reserved index file name (mirrors `core/indexes.ts`'s own private constant of the same name). */
|
|
33
|
-
const INDEX_FILE = "index.md";
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Read every existing `index.md` under the bundle as `bundle-relative-path → raw bytes` — the
|
|
37
|
-
* determinism seam `core/indexes.ts`'s `generateIndexes` splices into. Shared by `rename.ts` and
|
|
38
|
-
* `sync.ts`, both of which regenerate index hubs against whatever is currently on disk.
|
|
39
|
-
*/
|
|
40
|
-
export function readIndexBytes(docsRoot: string): Map<string, string> {
|
|
41
|
-
const bytes = new Map<string, string>();
|
|
42
|
-
for (const rel of walkMarkdown(docsRoot, undefined)) {
|
|
43
|
-
if (posix.basename(rel) === INDEX_FILE) {
|
|
44
|
-
bytes.set(rel, readSource(join(docsRoot, rel), `${DOCS_DIR}/${rel}`));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return bytes;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* A stable de-duplication key for an absolute file path: its `realpath` when resolvable (which folds
|
|
52
|
-
* case on a case-insensitive filesystem and collapses symlinks, so two spellings of one physical file
|
|
53
|
-
* — or a symlink and its target — share a key), else the path verbatim (a file that vanished mid-walk
|
|
54
|
-
* still gets a key, and {@link readSource} raises the real I/O error).
|
|
55
|
-
*/
|
|
56
|
-
export function canonicalIdentity(abs: string): string {
|
|
57
|
-
try {
|
|
58
|
-
return realpathSync.native(abs);
|
|
59
|
-
} catch {
|
|
60
|
-
return abs;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* The repo-relative POSIX path for a discovered file, for stable display and de-duplication. A path
|
|
66
|
-
* inside the repo renders relative (`docs/adr/x.md`); a path **outside** the repo keeps its absolute
|
|
67
|
-
* form (the relative path would escape with `../`). The `..` escape is matched by path **segment**, so
|
|
68
|
-
* a real in-repo file whose first segment merely starts with `..` (e.g. `..notes/x.md`) is not pushed
|
|
69
|
-
* out to its absolute form.
|
|
70
|
-
*/
|
|
71
|
-
export function toRepoRelative(root: string, abs: string): string {
|
|
72
|
-
const rel = relative(root, abs);
|
|
73
|
-
if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
74
|
-
return abs;
|
|
75
|
-
}
|
|
76
|
-
return rel.split(sep).join("/");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Whether `abs` resolves **inside** the repo `root` — the single repo-containment predicate the
|
|
81
|
-
* commands share, so the bundle-escape decision is made one way everywhere (a `/code-review max`
|
|
82
|
-
* finding: `validate` and `replace` had drifted on the `..`-segment edge). Matches
|
|
83
|
-
* {@link toRepoRelative}'s segment rule: a path equal to `..`, beginning `../`, or absolute after
|
|
84
|
-
* relativizing is outside; anything else is inside.
|
|
85
|
-
*/
|
|
86
|
-
export function withinRepo(root: string, abs: string): boolean {
|
|
87
|
-
const rel = relative(root, abs);
|
|
88
|
-
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
89
|
-
}
|