Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 20 additions & 28 deletions documents/PROJECT_ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ No command or service instantiates its own dependencies. All wiring is centraliz
- **Dependents**: `IConfigLoader`, `ConfigLoader`, `CommitBuilder`, `StalenessDetector`, `Validator`, `main.ts`, `path-query.ts`.

#### `src/types/query.ts`
- **Contains**: `TargetType` type, `QueryTarget` interface, `PathQueryOptions` interface, `SearchOptions` interface, `QueryResult` interface, `QueryMeta` interface.
- **Contains**: `TargetType` type, `QueryTarget` interface, `DiscoveryOptions` interface, `QueryOptions` interface, `QueryResult` interface, `QueryMeta` interface.
- **Single Responsibility**: Defines the shape of queries (inputs) and their results (outputs) for the query pipeline.
- **Dependencies**: `domain.ts` (for `LoreAtom`, `ConfidenceLevel`, etc.).
- **Dependents**: `PathResolver`, `AtomRepository`, `path-query.ts`, command files, formatters.
Expand Down Expand Up @@ -202,7 +202,7 @@ No command or service instantiates its own dependencies. All wiring is centraliz
- **Single Responsibility**: Central query engine. Retrieves `LoreAtom` objects from git history by target path, Lore-id, revision range, scope, or globally. Handles follow-link BFS traversal.
- **Dependencies**: `IGitClient`, `TrailerParser`, `domain.ts`, `query.ts`, `constants.ts`.
- **Dependents**: Commands (`context`, `constraints`, `rejected`, `directives`, `tested`, `search`, `log`, `stale`, `trace`, `squash`, `doctor`), `Validator`, `main.ts`.
- **Key methods**: `findByTarget()`, `findByLoreId()`, `findByRange()`, `findAll()`, `findByScope()`, `resolveFollowLinks()`.
- **Key methods**: `findByTarget()`, `findByLoreId()`, `findByRange()`, `findAll()`, `resolveFollowLinks()`.

#### `src/services/supersession-resolver.ts`
- **Contains**: `SupersessionResolver` class.
Expand Down Expand Up @@ -535,7 +535,7 @@ Each command calls `executePathQuery()` with its specific `visibleTrailers` para
**Gaps / Risks:**
- `AtomRepository` has the most methods of any service (7 public: `findByTarget`, `findByLoreId`, `findByCommitHash`, `findByRange`, `findAll`, `findByScope`, `resolveFollowLinks`). While all relate to "retrieving atoms from git," the `resolveFollowLinks` BFS traversal is a distinct concern that could be extracted into a `FollowLinkResolver` service.
- `Validator` has 10 validation rules implemented as private methods within a single class. If the rule set grows, a rule-based dispatch pattern (array of validation rule objects) would improve OCP compliance.
- Search filtering is extracted into the `SearchFilter` service class with `applyFilters()`, `atomHasTrailer()`, and `atomMatchesText()` methods.
- Search filtering is extracted into the `SearchFilter` service class which is injected into `AtomRepository`.

### O -- Open/Closed Principle

Expand Down Expand Up @@ -737,48 +737,40 @@ JsonFormatter() // no dependencies

## 6. Data Flow

### Query Flow: `lore constraints src/auth.ts`
### Query Flow: `lore search --author "cole" --text "auth"`

```mermaid
sequenceDiagram
actor User
participant CLI as Commander (main.ts)
participant CMD as constraints.ts
participant PQ as path-query.ts
participant PR as PathResolver
participant CMD as search.ts
participant AR as AtomRepository
participant GC as GitClient
participant TP as TrailerParser
participant SR as SupersessionResolver
participant FMT as Formatter

User->>CLI: lore constraints src/auth.ts
CLI->>CMD: route to constraints action
CMD->>PQ: executePathQuery("src/auth.ts", opts, deps, "constraints", ["Constraint"])
User->>CLI: lore search --author "cole" --text "auth"
CLI->>CMD: route to search action

PQ->>PR: parseTarget("src/auth.ts")
PR-->>PQ: QueryTarget { type: "file" }
PQ->>PR: toGitLogArgs(target)
PR-->>PQ: ["--", "src/auth.ts"]

PQ->>AR: findByTarget(gitLogArgs, options)
AR->>GC: log(["--", "src/auth.ts"])
CMD->>AR: findAll({ author: "cole", text: "auth" })
Note right of AR: Discovery Mode (Pass 1: Git)
AR->>GC: log(["--grep= Lore-id: ...", "--author=cole", "--grep=auth", "--all-match", ...])
GC-->>AR: RawCommit[]

Note right of AR: Discovery Mode (Pass 2: Application)
loop Each RawCommit
AR->>TP: containsLoreTrailers(trailers)
AR->>TP: parse(trailers, customKeys)
AR->>GC: getFilesChanged(hash)
AR->>TP: parse(trailers)
AR->>AR: applyFilters(atom) [Authoritative Pass]
end
AR-->>PQ: LoreAtom[]
AR-->>CMD: LoreAtom[]

PQ->>SR: resolve(atoms)
SR-->>PQ: Map‹loreId, SupersessionStatus›
PQ->>SR: filterActive(atoms, map)
SR-->>PQ: active LoreAtom[]
CMD->>SR: resolve(atoms)
SR-->>CMD: Map‹loreId, SupersessionStatus›

PQ->>FMT: formatQueryResult({ visibleTrailers: ["Constraint"] })
FMT-->>PQ: formatted string
PQ->>User: console.log(output)
CMD->>FMT: formatQueryResult()
FMT-->>User: console.log(output)
```

### Commit Flow: Agent pipes JSON to `lore commit`
Expand Down Expand Up @@ -1100,7 +1092,7 @@ Adding a new trailer type requires modifying all four locations. **Recommendatio

### `search.ts` Search Logic Extraction (Resolved)

Search filtering logic (`applyFilters`, `atomHasTrailer`, `atomMatchesText`) has been extracted into the `SearchFilter` service class (`src/services/search-filter.ts`). The command file now delegates to this service.
Search filtering logic (`applyFilters`, `atomHasTrailer`, `atomMatchesText`) has been re-extracted into the `SearchFilter` service and is now injected into `AtomRepository`. This maintains the performance of "Atom Discovery Mode" (Git pushdown) while adhering to SRP and reducing the complexity of the repository. `AtomRepository` coordinates the pipeline: pushing coarse filters to Git and delegating the authoritative second pass to `SearchFilter`.

---

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 10 additions & 8 deletions src/commands/helpers/path-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { PathResolver } from '../../services/path-resolver.js';
import type { IOutputFormatter } from '../../interfaces/output-formatter.js';
import type { LoreConfig } from '../../types/config.js';
import type { TrailerKey, LoreAtom, SupersessionStatus } from '../../types/domain.js';
import type { PathQueryOptions, QueryResult, TargetType } from '../../types/query.js';
import type { QueryOptions, QueryResult, TargetType } from '../../types/query.js';
import type { FormattableQueryResult } from '../../types/output.js';
import { buildQueryMeta } from './build-query-meta.js';

Expand Down Expand Up @@ -37,6 +37,7 @@ export interface PathQueryCommandOptions {
readonly limit?: number;
readonly maxCommits?: number;
readonly since?: string;
readonly until?: string;
}

/**
Expand All @@ -55,14 +56,15 @@ export async function executePathQuery(
): Promise<void> {
const { atomRepository, supersessionResolver, pathResolver, getFormatter, config } = deps;

const queryOptions: PathQueryOptions = {
const queryOptions: QueryOptions = {
scope: options.scope ?? null,
follow: options.follow ?? false,
all: options.all ?? false,
followLinks: options.follow ?? false,
includeSuperseded: options.all ?? false,
author: options.author ?? null,
limit: options.limit ?? null,
maxCommits: options.maxCommits ?? null,
since: options.since ?? null,
until: options.until ?? null,
};

// Step 1: Resolve target or use --scope
Expand All @@ -71,7 +73,7 @@ export async function executePathQuery(
let targetDisplay: string;

if (queryOptions.scope) {
atoms = await atomRepository.findByScope(queryOptions.scope, queryOptions);
atoms = await atomRepository.findAll(queryOptions);
targetType = 'global';
targetDisplay = `scope:${queryOptions.scope}`;
} else {
Expand All @@ -83,7 +85,7 @@ export async function executePathQuery(
}

// Step 2: Follow links if requested
if (queryOptions.follow && atoms.length > 0) {
if (queryOptions.followLinks && atoms.length > 0) {
atoms = await atomRepository.resolveFollowLinks(atoms, config.follow.maxDepth);
}

Expand All @@ -94,14 +96,14 @@ export async function executePathQuery(

// Step 4: Filter superseded atoms unless --all
let displayAtoms: readonly LoreAtom[];
if (queryOptions.all) {
if (queryOptions.includeSuperseded) {
displayAtoms = atoms;
} else {
displayAtoms = supersessionResolver.filterActive(atoms, supersessionMap);
}

// Step 4b: Apply result limit (--limit) after supersession filtering
if (queryOptions.limit !== null && queryOptions.limit > 0) {
if (queryOptions.limit !== null && queryOptions.limit !== undefined && queryOptions.limit > 0) {
displayAtoms = displayAtoms.slice(0, queryOptions.limit);
}

Expand Down
22 changes: 11 additions & 11 deletions src/commands/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Command } from 'commander';
import type { AtomRepository } from '../services/atom-repository.js';
import type { SupersessionResolver } from '../services/supersession-resolver.js';
import type { IOutputFormatter } from '../interfaces/output-formatter.js';
import type { PathQueryOptions, QueryResult } from '../types/query.js';
import type { QueryOptions, QueryResult } from '../types/query.js';
import type { LoreAtom } from '../types/domain.js';
import type { FormattableQueryResult } from '../types/output.js';
import { mergeOptions } from './helpers/merge-options.js';
Expand All @@ -13,6 +13,7 @@ interface LogCommandOptions {
readonly limit?: number;
readonly maxCommits?: number;
readonly since?: string;
readonly until?: string;
}

/**
Expand Down Expand Up @@ -42,24 +43,23 @@ export function registerLogCommand(

if (paths.length > 0) {
// Use git-level path filtering via findByTarget (#24)
const queryOptions: PathQueryOptions = {
const queryOptions: QueryOptions = {
scope: null,
follow: false,
all: false,
followLinks: false,
includeSuperseded: false,
author: null,
limit: null,
maxCommits: options.maxCommits ?? null,
since: options.since ?? null,
until: options.until ?? null,
};
atoms = await atomRepository.findByTarget(['--', ...paths], queryOptions);
} else {
const findOptions: { since?: string; maxCommits?: number } = {};
if (options.since) {
findOptions.since = options.since;
}
if (options.maxCommits !== undefined && options.maxCommits > 0) {
findOptions.maxCommits = options.maxCommits;
}
const findOptions: QueryOptions = {
...(options.since ? { since: options.since } : {}),
...(options.until ? { until: options.until } : {}),
...(options.maxCommits !== undefined && options.maxCommits > 0 ? { maxCommits: options.maxCommits } : {}),
};
atoms = await atomRepository.findAll(findOptions);
}

Expand Down
28 changes: 15 additions & 13 deletions src/commands/search.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type { Command } from 'commander';
import type { AtomRepository } from '../services/atom-repository.js';
import type { SupersessionResolver } from '../services/supersession-resolver.js';
import type { SearchFilter } from '../services/search-filter.js';
import type { IOutputFormatter } from '../interfaces/output-formatter.js';
import type { TrailerKey, LoreAtom, SupersessionStatus } from '../types/domain.js';
import type { SearchOptions, QueryResult } from '../types/query.js';
import type { QueryOptions, QueryResult } from '../types/query.js';
import type { FormattableQueryResult } from '../types/output.js';
import { mergeOptions } from './helpers/merge-options.js';
import { buildQueryMeta } from './helpers/build-query-meta.js';
Expand Down Expand Up @@ -34,7 +33,6 @@ export function registerSearchCommand(
deps: {
atomRepository: AtomRepository;
supersessionResolver: SupersessionResolver;
searchFilter: SearchFilter;
getFormatter: () => IOutputFormatter;
},
): void {
Expand All @@ -55,12 +53,12 @@ export function registerSearchCommand(
.option('--max-commits <n>', 'Maximum git commits to scan (supersession may be incomplete)', parsePositiveInt)
.action(async (_options: SearchCommandOptions, command: Command) => {
const options = mergeOptions<SearchCommandOptions>(command);
const { atomRepository, supersessionResolver, searchFilter, getFormatter } = deps;
const { atomRepository, supersessionResolver, getFormatter } = deps;

const searchOptions: SearchOptions = {
confidence: (options.confidence as SearchOptions['confidence']) ?? null,
scopeRisk: (options.scopeRisk as SearchOptions['scopeRisk']) ?? null,
reversibility: (options.reversibility as SearchOptions['reversibility']) ?? null,
const searchOptions: QueryOptions = {
confidence: (options.confidence as QueryOptions['confidence']) ?? null,
scopeRisk: (options.scopeRisk as QueryOptions['scopeRisk']) ?? null,
reversibility: (options.reversibility as QueryOptions['reversibility']) ?? null,
has: (options.has as TrailerKey) ?? null,
author: options.author ?? null,
scope: options.scope ?? null,
Expand All @@ -76,11 +74,15 @@ export function registerSearchCommand(
since: searchOptions.since ?? undefined,
until: searchOptions.until ?? undefined,
maxCommits: searchOptions.maxCommits ?? undefined,
author: searchOptions.author,
scope: searchOptions.scope,
has: searchOptions.has,
confidence: searchOptions.confidence,
scopeRisk: searchOptions.scopeRisk,
reversibility: searchOptions.reversibility,
text: searchOptions.text,
});

// Apply filters via SearchFilter service
atoms = searchFilter.applyFilters(atoms, searchOptions);

// Compute supersession on full set so each atom's status is available to the formatter
const supersessionMap: Map<string, SupersessionStatus> = supersessionResolver.resolve(atoms);

Expand All @@ -95,7 +97,7 @@ export function registerSearchCommand(
}

// Apply result limit (--limit) after all filtering and supersession
if (searchOptions.limit !== null && searchOptions.limit > 0) {
if (searchOptions.limit !== null && searchOptions.limit !== undefined && searchOptions.limit > 0) {
displayAtoms = displayAtoms.slice(0, searchOptions.limit);
}

Expand All @@ -118,7 +120,7 @@ export function registerSearchCommand(
});
}

function buildSearchTargetDescription(options: SearchOptions): string {
function buildSearchTargetDescription(options: QueryOptions): string {
const parts: string[] = [];

if (options.confidence) parts.push(`confidence=${options.confidence}`);
Expand Down
9 changes: 5 additions & 4 deletions src/commands/stale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { StalenessDetector } from '../services/staleness-detector.js';
import type { PathResolver } from '../services/path-resolver.js';
import type { IOutputFormatter } from '../interfaces/output-formatter.js';
import type { LoreAtom, SupersessionStatus } from '../types/domain.js';
import type { PathQueryOptions } from '../types/query.js';
import type { QueryOptions } from '../types/query.js';
import type { FormattableStalenessResult, StaleAtomReport } from '../types/output.js';
import { STALE_SIGNAL } from '../util/constants.js';
import { mergeOptions } from './helpers/merge-options.js';
Expand Down Expand Up @@ -46,14 +46,15 @@ export function registerStaleCommand(
if (target) {
const parsedTarget = pathResolver.parseTarget(target);
const gitLogArgs = pathResolver.toGitLogArgs(parsedTarget);
const queryOptions: PathQueryOptions = {
const queryOptions: QueryOptions = {
scope: null,
follow: false,
all: false,
followLinks: false,
includeSuperseded: false,
author: null,
limit: null,
maxCommits: null,
since: null,
until: null,
};
atoms = await atomRepository.findByTarget(gitLogArgs, queryOptions);
} else {
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/git-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@ export interface IGitClient {
getFilesChanged(commitHash: string): Promise<readonly string[]>;
countCommitsSince(path: string, sinceCommitHash: string): Promise<number>;
resolveRef(ref: string): Promise<string>;
resolveDate(dateStr: string): Promise<Date | null>;
getHeadMessage(): Promise<string>;
}
7 changes: 3 additions & 4 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { TrailerParser } from './services/trailer-parser.js';
import { PathResolver } from './services/path-resolver.js';
import { LoreIdGenerator } from './services/lore-id-generator.js';
import { ConfigLoader } from './services/config-loader.js';
import { SearchFilter } from './services/search-filter.js';
import { AtomRepository } from './services/atom-repository.js';
import { SupersessionResolver } from './services/supersession-resolver.js';
import { StalenessDetector } from './services/staleness-detector.js';
Expand All @@ -24,7 +25,6 @@ import { Validator } from './services/validator.js';
import { TerminalPrompt } from './services/terminal-prompt.js';
import { CommitInputResolver } from './services/commit-input-resolver.js';
import { HeadLoreIdReader } from './services/head-lore-id-reader.js';
import { SearchFilter } from './services/search-filter.js';

import { TextFormatter } from './formatters/text-formatter.js';
import { JsonFormatter } from './formatters/json-formatter.js';
Expand Down Expand Up @@ -93,13 +93,13 @@ async function main(): Promise<void> {
}

// 4. Create services that depend on others
const atomRepository = new AtomRepository(gitClient, trailerParser, config.trailers.custom);
const searchFilter = new SearchFilter();
const atomRepository = new AtomRepository(gitClient, trailerParser, searchFilter, config.trailers.custom);
const supersessionResolver = new SupersessionResolver();
const stalenessDetector = new StalenessDetector(gitClient, config);
const commitBuilder = new CommitBuilder(trailerParser, loreIdGenerator, config);
const squashMerger = new SquashMerger(loreIdGenerator);
const validator = new Validator(trailerParser, atomRepository, config);
const searchFilter = new SearchFilter();
const prompt = new TerminalPrompt();
const commitInputResolver = new CommitInputResolver(prompt);
const headLoreIdReader = new HeadLoreIdReader(gitClient, trailerParser);
Expand Down Expand Up @@ -148,7 +148,6 @@ async function main(): Promise<void> {
registerSearchCommand(program, {
atomRepository,
supersessionResolver,
searchFilter,
getFormatter,
});

Expand Down
Loading
Loading