@opum-ai/lore 0.1.0 → 0.1.1
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
package/src/core/log.ts
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* log.ts — the bundle's `log.md` as a **git-history-derived**, byte-stable artifact (LORE-47).
|
|
3
|
-
*
|
|
4
|
-
* Where `index.md` is regenerated from the bundle graph, `log.md` is a per-folder change log
|
|
5
|
-
* derived from the repository's own commit history: for each bundle folder, the commits that
|
|
6
|
-
* touched a file under it. This module owns two things:
|
|
7
|
-
*
|
|
8
|
-
* - **The `GitAdapter` seam** — the *third* injectable deterministic seam, alongside the clock
|
|
9
|
-
* and the Backlog subprocess (lore-design §8; [ADR-0014](../../docs/adr/0014-core-has-no-llm-dependency.md)).
|
|
10
|
-
* git is *local, deterministic computation* — not a network model — so reading history over a
|
|
11
|
-
* **pinned commit range** is reproducible, offline-safe, and air-gap-safe. Core depends only on
|
|
12
|
-
* the interface; the real adapter that shells `git log` is impure command-layer wiring (the same
|
|
13
|
-
* boundary the real `() => new Date()` clock sits on), supplied where `lore sync` is built. Tests
|
|
14
|
-
* inject a fake adapter returning a fixed fake history, never real `git`.
|
|
15
|
-
*
|
|
16
|
-
* - **The pure `generateLog`** — given a set of {@link GitCommit}s, produce the exact `log.md`
|
|
17
|
-
* bytes. It is total and order-independent: folders are directory-sorted and the commits under
|
|
18
|
-
* each are sorted by `(timestamp, hash)`, so the same history always yields byte-identical output
|
|
19
|
-
* (idempotent) regardless of the order the adapter returned commits in (lore-design §8: directory
|
|
20
|
-
* walks are sorted; no nondeterminism in core).
|
|
21
|
-
*
|
|
22
|
-
* **Drift-gate exemption (ADR-0007).** Because a git-derived `log.md` changes on *every* commit, it
|
|
23
|
-
* is materialized at **`lore sync`** time and **excluded** from `lore check`'s regenerate-and-compare
|
|
24
|
-
* drift gate. Gating it would report permanent drift (the gate's own commit would invalidate it) and
|
|
25
|
-
* break on shallow/read-only CI checkouts where full history is absent. `index.md` and the
|
|
26
|
-
* `<!-- lore:tasks -->` managed blocks stay gated as before.
|
|
27
|
-
*
|
|
28
|
-
* Per the core contract (lore-design §2.1) this module is pure: no filesystem, no spawn, no clock.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import { posix } from "node:path";
|
|
32
|
-
import { singleLine } from "../errors";
|
|
33
|
-
import { compareCodeUnits } from "./order";
|
|
34
|
-
import { DOCS_DIR } from "./scaffold";
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* One commit as the {@link GitAdapter} surfaces it — the minimal, deterministic projection
|
|
38
|
-
* `generateLog` needs. All fields are stable for a given commit in a given repository.
|
|
39
|
-
*/
|
|
40
|
-
export interface GitCommit {
|
|
41
|
-
/** The commit hash (full or a stable abbreviation). Emitted verbatim and used as the sort tiebreak. */
|
|
42
|
-
readonly hash: string;
|
|
43
|
-
/** The committer date as an ISO-8601 string with offset (never a `Date`, mirroring ADR-0006 §2). */
|
|
44
|
-
readonly timestamp: string;
|
|
45
|
-
/** The commit message's first line (subject). Treated as single-line; any stray newline is collapsed. */
|
|
46
|
-
readonly subject: string;
|
|
47
|
-
/** The repo-relative POSIX paths the commit touched (in any order; `generateLog` groups + sorts). */
|
|
48
|
-
readonly files: readonly string[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* A **pinned** commit range — the determinism boundary. `to` is the inclusive upper bound (a tag
|
|
53
|
-
* or sha, e.g. `HEAD` resolved to a sha by the caller); `from`, when given, is the exclusive lower
|
|
54
|
-
* bound. Pinning both ends is what makes a generated `log.md` reproducible: the same range over the
|
|
55
|
-
* same repository yields the same commits.
|
|
56
|
-
*/
|
|
57
|
-
export interface GitLogRange {
|
|
58
|
-
/** Exclusive lower bound (a tag/sha); absent → from the start of history. */
|
|
59
|
-
readonly from?: string;
|
|
60
|
-
/** Inclusive upper bound (a tag/sha) — pin this to a concrete sha for a reproducible log. */
|
|
61
|
-
readonly to: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* The injectable git seam (lore-design §8, the third after clock + Backlog). The real
|
|
66
|
-
* implementation shells `git log` and lives at the command layer (impure, like the real clock);
|
|
67
|
-
* tests inject a fake returning a fixed fake history. Core knows only this interface.
|
|
68
|
-
*/
|
|
69
|
-
export interface GitAdapter {
|
|
70
|
-
/**
|
|
71
|
-
* The commits touching `root` within `range`, in any order. `root` is a **pathspec**, not a
|
|
72
|
-
* post-filter hint: an implementation is expected to scope the underlying `git log` walk itself
|
|
73
|
-
* (e.g. `-- docs`) so a commit that never touched anything under `root` is never even considered,
|
|
74
|
-
* let alone returned — the real adapter (`adapters/git.ts`) does exactly that, so `lore sync`
|
|
75
|
-
* never walks or buffers unrelated repository history just to build a docs-scoped `log.md`.
|
|
76
|
-
* Optional only so a fake ignoring it stays a one-liner; {@link buildLog} always passes it.
|
|
77
|
-
*/
|
|
78
|
-
history(range: GitLogRange, root?: string): readonly GitCommit[];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Options for {@link generateLog}. */
|
|
82
|
-
export interface GenerateLogOptions {
|
|
83
|
-
/**
|
|
84
|
-
* The bundle root the log is scoped to (default {@link DOCS_DIR}, `"docs"`). A commit's files
|
|
85
|
-
* outside this root are ignored, so `log.md` reflects bundle history, not unrelated source churn.
|
|
86
|
-
* Compared by path segment, so `"docs"` matches `docs/x.md` but never a sibling like `docsite/x.md`.
|
|
87
|
-
* An empty string falls back to the default (an empty root would match nothing).
|
|
88
|
-
*/
|
|
89
|
-
readonly root?: string;
|
|
90
|
-
/** The document's top-level heading (default `"Change log"`). */
|
|
91
|
-
readonly title?: string;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** One commit as it renders in `log.md` — the commit's identity plus its subject collapsed once. */
|
|
95
|
-
interface LogEntry {
|
|
96
|
-
readonly hash: string;
|
|
97
|
-
readonly timestamp: string;
|
|
98
|
-
readonly subject: string;
|
|
99
|
-
/**
|
|
100
|
-
* The absolute instant the {@link timestamp} denotes (epoch ms), or `NaN` when it carries no
|
|
101
|
-
* explicit offset (so a host-local — machine-dependent — parse is never trusted) or is
|
|
102
|
-
* unparseable. Computed once at construction so {@link compareEntries} parses each commit's
|
|
103
|
-
* timestamp **once** total, not O(N log N) times inside the sort comparator.
|
|
104
|
-
*/
|
|
105
|
-
readonly instant: number;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Build a `log.md`'s bytes from a {@link GitAdapter} over a pinned range — the function `lore sync`
|
|
110
|
-
* calls with the real adapter and tests call with a fake one, so the seam is exercised end-to-end.
|
|
111
|
-
* The same resolved `root` that scopes `log.md`'s content is also passed to `adapter.history` as a
|
|
112
|
-
* pathspec, so the real adapter can scope the underlying `git log` walk itself — not just the
|
|
113
|
-
* `generateLog` grouping below — to the bundle root (LORE-143). The pure {@link generateLog} does
|
|
114
|
-
* the byte computation; this only resolves the history through the seam.
|
|
115
|
-
*/
|
|
116
|
-
export function buildLog(adapter: GitAdapter, range: GitLogRange, options: GenerateLogOptions = {}): string {
|
|
117
|
-
return generateLog(adapter.history(range, resolveRoot(options.root)), options);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Render the byte-stable `log.md` for `commits`: group each commit under every bundle folder it
|
|
122
|
-
* touched (the immediate parent directory of a touched file under the bundle root), emit folders in
|
|
123
|
-
* directory-sorted order, and under each folder list its commits sorted by `(timestamp, hash)`.
|
|
124
|
-
*
|
|
125
|
-
* Pure, total, and order-independent — the same set of commits always produces byte-identical
|
|
126
|
-
* output, so re-running `lore sync` with no new history is a byte-level no-op. A commit that touched
|
|
127
|
-
* no file under the bundle root contributes nothing (it never appears). An empty history yields just
|
|
128
|
-
* the heading, so the file is always well-formed.
|
|
129
|
-
*/
|
|
130
|
-
export function generateLog(commits: readonly GitCommit[], options: GenerateLogOptions = {}): string {
|
|
131
|
-
const root = resolveRoot(options.root);
|
|
132
|
-
const title = options.title ?? "Change log";
|
|
133
|
-
|
|
134
|
-
// folder → the commits touching it. `foldersTouched` already returns a *set* of folders, so each
|
|
135
|
-
// commit is appended at most once per folder — no dedup keyed by hash, which would otherwise
|
|
136
|
-
// collapse two genuinely distinct commits that share an abbreviated hash. The subject is collapsed
|
|
137
|
-
// once here (per commit), not once per folder it lands in.
|
|
138
|
-
const byFolder = new Map<string, LogEntry[]>();
|
|
139
|
-
for (const commit of commits) {
|
|
140
|
-
const folders = foldersTouched(commit.files, root);
|
|
141
|
-
if (folders.size === 0) {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
const entry: LogEntry = {
|
|
145
|
-
hash: commit.hash,
|
|
146
|
-
timestamp: commit.timestamp,
|
|
147
|
-
subject: singleLine(commit.subject),
|
|
148
|
-
instant: toInstant(commit.timestamp),
|
|
149
|
-
};
|
|
150
|
-
for (const folder of folders) {
|
|
151
|
-
const bucket = byFolder.get(folder);
|
|
152
|
-
if (bucket === undefined) {
|
|
153
|
-
byFolder.set(folder, [entry]);
|
|
154
|
-
} else {
|
|
155
|
-
bucket.push(entry);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const sections = [...byFolder.entries()]
|
|
161
|
-
.sort(([a], [b]) => compareCodeUnits(a, b))
|
|
162
|
-
.map(([folder, entries]) => {
|
|
163
|
-
const lines = entries.sort(compareEntries).map((e) => `- ${e.timestamp} ${e.hash} ${e.subject}`);
|
|
164
|
-
return `## ${folder}\n\n${lines.join("\n")}\n`;
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
return [`# ${title}\n`, ...sections].join("\n");
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Normalize a {@link GenerateLogOptions.root}/{@link GitAdapter.history} root to the bundle root it
|
|
172
|
-
* denotes, so equivalent spellings of the same root canonicalize identically (LORE-243): a leading
|
|
173
|
-
* `./`, a trailing `/.`, and internal redundant separators (`//`, `/./`) are all collapsed via
|
|
174
|
-
* {@link posix.normalize} before the trailing slash(es) are stripped — so `./docs`, `docs/.`,
|
|
175
|
-
* `./docs/`, and `docs//adr`/`docs/./adr` all resolve exactly like `docs`/`docs/adr`. Without the
|
|
176
|
-
* `posix.normalize` pass, the `${root}/` probe in {@link isUnderRoot} would compare against the
|
|
177
|
-
* un-normalized root and match nothing — a silently empty log. Falls back to {@link DOCS_DIR} for an
|
|
178
|
-
* absent, empty, or all-slashes root (`normalize` reduces those to `"."` or `"/"`, neither of which
|
|
179
|
-
* would match anything as a bundle root). Shared by {@link generateLog} (post-filtering) and
|
|
180
|
-
* {@link buildLog} (the pathspec handed to {@link GitAdapter.history}) so the two always agree on
|
|
181
|
-
* exactly which root scopes a given `log.md`.
|
|
182
|
-
*/
|
|
183
|
-
function resolveRoot(root: string | undefined): string {
|
|
184
|
-
const normalized = posix.normalize(root || DOCS_DIR).replace(/\/+$/, "");
|
|
185
|
-
return normalized === "" || normalized === "." ? DOCS_DIR : normalized;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* The distinct bundle folders a commit's `files` touch, scoped to `root`. A folder is the immediate
|
|
190
|
-
* parent directory of a touched file (`docs/adr/0014.md` → `docs/adr`). A file directly under the
|
|
191
|
-
* root (`docs/index.md` → `docs`) groups under the root itself. Files outside the root are dropped.
|
|
192
|
-
*/
|
|
193
|
-
function foldersTouched(files: readonly string[], root: string): Set<string> {
|
|
194
|
-
const folders = new Set<string>();
|
|
195
|
-
for (const file of files) {
|
|
196
|
-
if (isUnderRoot(file, root)) {
|
|
197
|
-
folders.add(posix.dirname(file));
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return folders;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* True when `file` sits **strictly under** the bundle root (`<root>/…`). A file whose path *equals*
|
|
205
|
-
* the root is deliberately excluded: it is a file literally named `docs` (not a bundle document), and
|
|
206
|
-
* `posix.dirname` would put it under the root's *parent* (`.`), emitting a section above the bundle.
|
|
207
|
-
* A document directly under the root — `docs/index.md` — still matches and groups under `## docs`.
|
|
208
|
-
*/
|
|
209
|
-
function isUnderRoot(file: string, root: string): boolean {
|
|
210
|
-
return file.startsWith(`${root}/`);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** Matches the offset suffix of an ISO-8601 timestamp — `Z`, or `±HH:MM`/`±HHMM` (colon optional). */
|
|
214
|
-
const ISO_OFFSET = /(?:Z|[+-]\d{2}:?\d{2})$/;
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* The absolute instant `timestamp` denotes, in epoch milliseconds, or `NaN` when it does not denote
|
|
218
|
-
* one. Only an **offset-bearing** ISO-8601 timestamp (the {@link GitCommit.timestamp} contract) is a
|
|
219
|
-
* fixed instant; an offset-*less* one would be parsed in the host's local time zone — a
|
|
220
|
-
* machine-dependent result that has no place in byte-stable output — so it is reported as `NaN` and
|
|
221
|
-
* left to the deterministic text tiebreak, never trusted as a number.
|
|
222
|
-
*/
|
|
223
|
-
function toInstant(timestamp: string): number {
|
|
224
|
-
return ISO_OFFSET.test(timestamp) ? Date.parse(timestamp) : Number.NaN;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Order entries by ascending absolute **instant**, then by deterministic, machine-independent
|
|
229
|
-
* tiebreaks. Timestamps are ISO-8601 *with offset* (see {@link GitCommit.timestamp}), so two equal
|
|
230
|
-
* instants written in different offsets compare equal — a lexical compare would order them by
|
|
231
|
-
* wall-clock text instead. Each entry's instant is precomputed ({@link LogEntry.instant}); when both
|
|
232
|
-
* are real instants they order numerically, and an entry with a real instant always precedes one
|
|
233
|
-
* without (an offset-less/unparseable timestamp). On an exact instant tie — or when neither has an
|
|
234
|
-
* instant — the order falls through to `(timestamp, hash, subject)` code-unit compares, so it stays
|
|
235
|
-
* **total and reproducible** (the subject tiebreak keeps two commits sharing an instant *and* an
|
|
236
|
-
* abbreviated hash from falling back to input order).
|
|
237
|
-
*/
|
|
238
|
-
function compareEntries(a: LogEntry, b: LogEntry): number {
|
|
239
|
-
const aHasInstant = !Number.isNaN(a.instant);
|
|
240
|
-
const bHasInstant = !Number.isNaN(b.instant);
|
|
241
|
-
if (aHasInstant && bHasInstant) {
|
|
242
|
-
if (a.instant !== b.instant) {
|
|
243
|
-
return a.instant < b.instant ? -1 : 1;
|
|
244
|
-
}
|
|
245
|
-
} else if (aHasInstant !== bHasInstant) {
|
|
246
|
-
return aHasInstant ? -1 : 1;
|
|
247
|
-
}
|
|
248
|
-
return (
|
|
249
|
-
compareCodeUnits(a.timestamp, b.timestamp) ||
|
|
250
|
-
compareCodeUnits(a.hash, b.hash) ||
|
|
251
|
-
compareCodeUnits(a.subject, b.subject)
|
|
252
|
-
);
|
|
253
|
-
}
|