@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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +306 -0
  3. package/bin/lore.cjs +109 -0
  4. package/package.json +67 -0
  5. package/src/adapters/backlog.ts +1084 -0
  6. package/src/adapters/git.ts +221 -0
  7. package/src/cli.ts +667 -0
  8. package/src/commands/agent.ts +301 -0
  9. package/src/commands/agents.ts +302 -0
  10. package/src/commands/args.ts +209 -0
  11. package/src/commands/changed.ts +70 -0
  12. package/src/commands/check.ts +1031 -0
  13. package/src/commands/codex-bridge.ts +49 -0
  14. package/src/commands/concurrency.ts +48 -0
  15. package/src/commands/context.ts +292 -0
  16. package/src/commands/discover.ts +89 -0
  17. package/src/commands/explorer.ts +253 -0
  18. package/src/commands/export.ts +93 -0
  19. package/src/commands/fswrite.ts +928 -0
  20. package/src/commands/graph.ts +291 -0
  21. package/src/commands/help.ts +151 -0
  22. package/src/commands/impact.ts +59 -0
  23. package/src/commands/init.ts +583 -0
  24. package/src/commands/instructions.ts +91 -0
  25. package/src/commands/link.ts +929 -0
  26. package/src/commands/new.ts +476 -0
  27. package/src/commands/orphans.ts +457 -0
  28. package/src/commands/path.ts +67 -0
  29. package/src/commands/provenance.ts +68 -0
  30. package/src/commands/query.ts +312 -0
  31. package/src/commands/reconcile-shared.ts +280 -0
  32. package/src/commands/rename.ts +585 -0
  33. package/src/commands/replace.ts +320 -0
  34. package/src/commands/scaffold.ts +346 -0
  35. package/src/commands/schema.ts +293 -0
  36. package/src/commands/snapshot.ts +130 -0
  37. package/src/commands/supersede.ts +400 -0
  38. package/src/commands/sync.ts +371 -0
  39. package/src/commands/tasks.ts +271 -0
  40. package/src/commands/traversal.ts +151 -0
  41. package/src/commands/validate.ts +226 -0
  42. package/src/config.ts +598 -0
  43. package/src/core/agent-bridge.ts +287 -0
  44. package/src/core/agent-context.ts +498 -0
  45. package/src/core/agent-profile.ts +447 -0
  46. package/src/core/bundle.ts +893 -0
  47. package/src/core/check.ts +853 -0
  48. package/src/core/codex-bridge.ts +100 -0
  49. package/src/core/concept.ts +597 -0
  50. package/src/core/consumer-scaffold.ts +433 -0
  51. package/src/core/context.ts +271 -0
  52. package/src/core/explorer-contract.ts +441 -0
  53. package/src/core/explorer-qualification.ts +58 -0
  54. package/src/core/explorer.ts +518 -0
  55. package/src/core/finding.ts +31 -0
  56. package/src/core/graph.ts +201 -0
  57. package/src/core/indexes.ts +436 -0
  58. package/src/core/instructions.ts +209 -0
  59. package/src/core/ladybug-driver.ts +1795 -0
  60. package/src/core/ladybug-lifecycle.ts +1178 -0
  61. package/src/core/ladybug-native.ts +95 -0
  62. package/src/core/ladybug-source.ts +667 -0
  63. package/src/core/links.ts +681 -0
  64. package/src/core/log.ts +253 -0
  65. package/src/core/managed-block.ts +540 -0
  66. package/src/core/manifest.ts +718 -0
  67. package/src/core/order.ts +13 -0
  68. package/src/core/profile.ts +1007 -0
  69. package/src/core/projection.ts +195 -0
  70. package/src/core/query.ts +542 -0
  71. package/src/core/reconcile.ts +236 -0
  72. package/src/core/replace.ts +419 -0
  73. package/src/core/retrieval.ts +213 -0
  74. package/src/core/rewrite.ts +940 -0
  75. package/src/core/scaffold.ts +255 -0
  76. package/src/core/schema.ts +366 -0
  77. package/src/core/snapshot-runtime.ts +52 -0
  78. package/src/core/snapshot-store.ts +287 -0
  79. package/src/core/snapshot.ts +711 -0
  80. package/src/core/template.ts +429 -0
  81. package/src/core/traversal.ts +487 -0
  82. package/src/core/validate.ts +517 -0
  83. package/src/core/workspace-contract.ts +473 -0
  84. package/src/core/workspace-projection.ts +365 -0
  85. package/src/core/workspace-retrieval.ts +196 -0
  86. package/src/core/workspace-source.ts +174 -0
  87. package/src/errors.ts +697 -0
  88. package/src/meta.ts +7 -0
  89. package/src/output.ts +589 -0
  90. package/src/scripts/upstream-backlog-watch.ts +288 -0
  91. package/src/state.ts +390 -0
@@ -0,0 +1,209 @@
1
+ /**
2
+ * commands/args.ts — the shared Commander-backed command parser.
3
+ *
4
+ * Command names and options come from `core/manifest.ts`, the same declaration
5
+ * source used by the top-level Commander graph and generated help. Thin command
6
+ * handlers retain only command-specific arity/value/business validation.
7
+ */
8
+
9
+ import { posix } from "node:path";
10
+ import { Command, CommanderError, Option } from "commander";
11
+ import { findManifestCommand, type ManifestFlag } from "../core/manifest";
12
+ import { RESERVED_STEMS } from "../core/scaffold";
13
+ import type { WorkspaceRetrievalSelection } from "../core/workspace-retrieval";
14
+ import { LoreError } from "../errors";
15
+
16
+ /** Commander parse failure enriched by the local subcommand that raised it. */
17
+ interface CommandScopedError extends CommanderError {
18
+ loreCommand?: string;
19
+ }
20
+
21
+ /** The parsed form of a command's arguments. */
22
+ export interface ParsedArgs {
23
+ /** Every non-flag token, in order (everything after a `--` marker included verbatim). */
24
+ readonly positionals: string[];
25
+ /** The set of recognized `--flag` names that were passed. */
26
+ readonly flags: ReadonlySet<string>;
27
+ /** Every value occurrence for each value-taking flag, in command-line order. */
28
+ readonly values: ReadonlyMap<string, readonly string[]>;
29
+ /** Number of times each option occurred, including boolean switches. */
30
+ readonly counts: ReadonlyMap<string, number>;
31
+ }
32
+
33
+ /**
34
+ * Parse one command's tokens with a fresh local Commander instance sourced from
35
+ * its manifest declaration. A variadic positional captures all operands so each
36
+ * thin handler can preserve its established arity wording and input metadata.
37
+ */
38
+ export function parseCommandArgs(args: readonly string[], command: string): ParsedArgs {
39
+ const definition = findManifestCommand(command);
40
+ if (definition === undefined) {
41
+ throw new Error(`missing CLI manifest definition for command "${command}"`);
42
+ }
43
+ const flags = new Set<string>();
44
+ const values = new Map<string, string[]>();
45
+ const counts = new Map<string, number>();
46
+ const parser = new Command(command)
47
+ .helpOption(false)
48
+ .helpCommand(false)
49
+ .showSuggestionAfterError(false)
50
+ .exitOverride()
51
+ .configureOutput({ writeOut: () => {}, writeErr: () => {} })
52
+ .argument("[args...]")
53
+ .action(() => {});
54
+ for (const flag of definition.flags) {
55
+ parser.addOption(commanderOption(flag));
56
+ parser.on(`option:${flag.name}`, (value?: string) => {
57
+ flags.add(flag.name);
58
+ counts.set(flag.name, (counts.get(flag.name) ?? 0) + 1);
59
+ if (value !== undefined) {
60
+ const collected = values.get(flag.name) ?? [];
61
+ collected.push(value);
62
+ values.set(flag.name, collected);
63
+ }
64
+ });
65
+ }
66
+ try {
67
+ parser.parse([...args], { from: "user" });
68
+ } catch (error) {
69
+ throw commanderUsageError(error, command);
70
+ }
71
+ return { positionals: [...parser.args], flags, values, counts };
72
+ }
73
+
74
+ /** Commander flag declaration sourced from one manifest flag. */
75
+ export function optionSyntax(flag: ManifestFlag): string {
76
+ const long = `--${flag.name}${flag.takesValue ? " <value>" : ""}`;
77
+ return flag.alias === undefined ? long : `-${flag.alias}, ${long}`;
78
+ }
79
+
80
+ /**
81
+ * Build one Commander option with Lore's compatibility rule for separated
82
+ * values: a required option must not consume a following long/global flag.
83
+ * Commander normally accepts such tokens as values; Lore has always reported
84
+ * the preceding option as value-less instead.
85
+ */
86
+ export function commanderOption(flag: ManifestFlag): Option {
87
+ const option = new Option(optionSyntax(flag), flag.summary);
88
+ if (flag.takesValue) {
89
+ option.argParser((value) => {
90
+ if (value.startsWith("--") || value === "-h" || value === "-v") {
91
+ throw usage(`--${flag.name} needs a value`, `pass a value, e.g. --${flag.name}=<value>`);
92
+ }
93
+ return value;
94
+ });
95
+ }
96
+ return option;
97
+ }
98
+
99
+ /** Extract an unknown command from a Commander failure. */
100
+ export function commanderUnknownCommand(error: unknown): string | undefined {
101
+ if (!(error instanceof CommanderError) || error.code !== "commander.unknownCommand") {
102
+ return undefined;
103
+ }
104
+ return /unknown command '([^']+)'/.exec(error.message)?.[1];
105
+ }
106
+
107
+ /** Translate a caught Commander parser failure into Lore's semantic usage error. */
108
+ export function commanderUsageError(error: unknown, command?: string): LoreError {
109
+ if (!(error instanceof CommanderError)) {
110
+ throw error;
111
+ }
112
+ const scopedCommand = command ?? (error as CommandScopedError).loreCommand;
113
+ const unknownCommand = commanderUnknownCommand(error);
114
+ if (unknownCommand !== undefined) {
115
+ return new LoreError("usage", `unknown command "${unknownCommand}"`, "run `lore --help` to list commands", {
116
+ command: unknownCommand,
117
+ });
118
+ }
119
+ const unknownOption = /unknown option '([^']+)'/.exec(error.message)?.[1];
120
+ if (unknownOption !== undefined) {
121
+ const equals = unknownOption.indexOf("=");
122
+ if (scopedCommand !== undefined && equals > 0) {
123
+ const option = unknownOption.slice(0, equals);
124
+ const name = option.startsWith("--") ? option.slice(2) : option;
125
+ const declared = findManifestCommand(scopedCommand)?.flags.find((flag) => flag.name === name);
126
+ if (declared !== undefined && !declared.takesValue) {
127
+ return usage(`${option} takes no value`, `pass ${option} on its own`);
128
+ }
129
+ }
130
+ const scope = scopedCommand === undefined ? "lore --help" : `lore ${scopedCommand} --help`;
131
+ return new LoreError("usage", `unknown option "${unknownOption}"`, `run \`${scope}\` to list options`, {
132
+ options: [unknownOption],
133
+ });
134
+ }
135
+ const missingValue = /option '([^']+)' argument missing/.exec(error.message)?.[1];
136
+ if (missingValue !== undefined) {
137
+ const flag = missingValue.split(/[ ,]/, 1)[0] as string;
138
+ return usage(`${flag} needs a value`, `pass a value, e.g. ${flag}=<value>`);
139
+ }
140
+ return usage(
141
+ error.message.replace(/^error:\s*/, ""),
142
+ scopedCommand === undefined
143
+ ? "run `lore --help` to list commands and options"
144
+ : `run \`lore ${scopedCommand} --help\``,
145
+ );
146
+ }
147
+
148
+ /** Return all values for a flag, or an empty array when it was absent. */
149
+ export function optionValues(parsed: ParsedArgs, name: string): readonly string[] {
150
+ return parsed.values.get(name) ?? [];
151
+ }
152
+
153
+ /** Return a single flag value, rejecting a duplicate with the established Lore wording. */
154
+ export function singleOptionValue(parsed: ParsedArgs, name: string): string | undefined {
155
+ const values = optionValues(parsed, name);
156
+ if (values.length > 1) {
157
+ throw usage(`--${name} given more than once`, `pass --${name} at most once`);
158
+ }
159
+ return values[0];
160
+ }
161
+
162
+ /** Reject duplicate boolean occurrences for commands whose prior contract did so. */
163
+ export function assertFlagAtMostOnce(parsed: ParsedArgs, name: string): void {
164
+ if ((parsed.counts.get(name) ?? 0) > 1) {
165
+ throw usage(`--${name} given more than once`, `pass --${name} at most once`);
166
+ }
167
+ }
168
+
169
+ /** Parse the shared explicit workspace/repository selection flags. */
170
+ export function workspaceSelection(parsed: ParsedArgs): WorkspaceRetrievalSelection | undefined {
171
+ const rawManifest = singleOptionValue(parsed, "workspace");
172
+ const memberIds = optionValues(parsed, "repository").map((value) => value.trim());
173
+ if (rawManifest === "" || rawManifest?.trim() === "") {
174
+ throw usage("--workspace needs a value", "pass an explicit lore-workspace-manifest/1 JSON file");
175
+ }
176
+ if (memberIds.some((memberId) => memberId === "")) {
177
+ throw usage("--repository needs a value", "pass a member id declared by the explicit workspace manifest");
178
+ }
179
+ if (rawManifest === undefined) {
180
+ if (memberIds.length > 0)
181
+ throw usage("--repository requires --workspace", "select an explicit workspace manifest first");
182
+ return undefined;
183
+ }
184
+ if (new Set(memberIds).size !== memberIds.length) {
185
+ throw usage("--repository values must be unique", "pass each selected workspace member at most once");
186
+ }
187
+ return { manifestPath: rawManifest.trim(), memberIds };
188
+ }
189
+
190
+ /** A `usage` {@link LoreError} (exit `2`) with an actionable hint. */
191
+ export function usage(message: string, hint: string, input?: unknown): LoreError {
192
+ return new LoreError("usage", message, hint, input);
193
+ }
194
+
195
+ /**
196
+ * Reject a reserved hub stem (`index`/`log`) as a `<action>` target/principal — a `usage` error.
197
+ * Shared by `rename.ts`, `supersede.ts`, and `link.ts` so the policy (which stems are reserved, and
198
+ * its wording) can't drift across the three independently. `action` is the verb phrase for the
199
+ * message, e.g. `"rename to"`, `"supersede"`, `"link"`/`"unlink"`.
200
+ */
201
+ export function assertNotReservedStem(id: string, action: string): void {
202
+ if (RESERVED_STEMS.has(posix.basename(id))) {
203
+ throw usage(
204
+ `cannot ${action} "${id}": "${posix.basename(id)}" is a reserved, machine-generated file name`,
205
+ "index.md/log.md are generated by lore, not authored concepts",
206
+ { id },
207
+ );
208
+ }
209
+ }
@@ -0,0 +1,70 @@
1
+ /** `lore changed`: bounded deterministic retained-snapshot comparison. */
2
+
3
+ import type { BacklogAdapter } from "../adapters/backlog";
4
+ import { type ChangedResult, compareRetainedSnapshots, type RetainedFactKind } from "../core/snapshot";
5
+ import { resolveSnapshotScope } from "../core/snapshot-runtime";
6
+ import type { SnapshotScopeSelection } from "../core/snapshot-store";
7
+ import { loadSnapshot } from "../core/snapshot-store";
8
+ import { WarningCollector, type Writer } from "../errors";
9
+ import { emit, type OutputContext, type Renderable } from "../output";
10
+ import { optionValues, parseCommandArgs, singleOptionValue, usage, workspaceSelection } from "./args";
11
+
12
+ export interface ChangedCommandOptions {
13
+ readonly root: string;
14
+ readonly output: OutputContext;
15
+ readonly args: readonly string[];
16
+ readonly stdout?: Writer;
17
+ readonly stderr?: Writer;
18
+ readonly adapter?: BacklogAdapter;
19
+ readonly resolveScope?: () => SnapshotScopeSelection | Promise<SnapshotScopeSelection>;
20
+ }
21
+
22
+ export async function runChanged(options: ChangedCommandOptions): Promise<number> {
23
+ const parsed = parseCommandArgs(options.args, "changed");
24
+ const workspace = workspaceSelection(parsed);
25
+ if (parsed.positionals.length !== 2)
26
+ throw usage("changed needs exactly <from> and <to>", "run `lore changed <from> <to> [--workspace <manifest>]`");
27
+ const limit = parseLimit(singleOptionValue(parsed, "limit"));
28
+ const kinds = optionValues(parsed, "kind").map(parseKind);
29
+ const advisories = new WarningCollector();
30
+ const selection = workspace === undefined ? {} : { workspace: workspace.manifestPath };
31
+ const scope =
32
+ options.resolveScope === undefined
33
+ ? await resolveSnapshotScope({ root: options.root, selection, warnings: advisories, adapter: options.adapter })
34
+ : await options.resolveScope();
35
+ const from = loadSnapshot(options.root, scope, parsed.positionals[0] as string);
36
+ const to = loadSnapshot(options.root, scope, parsed.positionals[1] as string);
37
+ const data = compareRetainedSnapshots(from, to, {
38
+ limit,
39
+ repositories: workspace?.memberIds ?? [],
40
+ ...(kinds.length > 0 ? { kinds } : {}),
41
+ });
42
+ advisories.flush({ color: options.output.color, stderr: options.stderr });
43
+ emit(changedRenderable(data), options.output, options.stdout);
44
+ return 0;
45
+ }
46
+
47
+ function parseLimit(raw: string | undefined): number | undefined {
48
+ if (raw === undefined) return undefined;
49
+ if (!/^[0-9]+$/u.test(raw)) throw usage("--limit must be an integer", "pass a value from 1 through 1000");
50
+ return Number(raw);
51
+ }
52
+
53
+ function parseKind(value: string): RetainedFactKind {
54
+ if (value !== "concept" && value !== "task" && value !== "edge")
55
+ throw usage(`unknown changed kind ${JSON.stringify(value)}`, "use concept, task, or edge");
56
+ return value;
57
+ }
58
+
59
+ function changedRenderable(data: ChangedResult): Renderable<ChangedResult> {
60
+ return { kind: "changed.result", data, pretty: renderChanged, plain: renderChanged };
61
+ }
62
+
63
+ function renderChanged(data: ChangedResult): string {
64
+ const header = `changed ${data.from.snapshotKey}..${data.to.snapshotKey}: ${data.totalChanges} changes, ${data.shown} shown${data.truncated ? " (truncated)" : ""}`;
65
+ const rows = data.changes.map(
66
+ (change) =>
67
+ `${change.change} ${change.recordKind} ${change.id}${change.fieldsChanged.length > 0 ? ` [${change.fieldsChanged.join(",")}]` : ""}`,
68
+ );
69
+ return [header, ...rows].join("\n");
70
+ }