Skip to content

Commit eb78568

Browse files
GoBeromsuclaude
andauthored
refactor: migrate to layered architecture (domain/ui/types/utils) (#43)
- Move business logic (classifier, provider core, constants) to domain/ - Move pure utilities (sanitizer, frontmatter, ErrorHandler) to utils/ - Move types to types/ - Move Obsidian-dependent code (settings UI, OAuth, commands) to ui/ - Delete old directories: classifier/, provider/, settings/, lib/ - ESLint no-restricted-imports enforces domain/types/utils purity - All 214 tests pass Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4d16fbe commit eb78568

58 files changed

Lines changed: 462 additions & 395 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

__tests__/classifier/ClassificationService.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
1-
import { ClassificationService, ClassificationContext } from '../../src/classifier/ClassificationService';
1+
import { ClassificationService, ClassificationContext } from '../../src/ui/ClassificationService';
22
import type { App, TFile, MetadataCache, Vault } from 'obsidian';
33
import type { FrontmatterField, ProviderConfig, OAuthTokens } from '../../src/types';
4-
import { processAPIRequest } from '../../src/provider';
5-
import { insertToFrontMatter, getFieldValues } from '../../src/lib/frontmatter';
6-
import { getPromptTemplate } from '../../src/provider/prompt';
4+
import { processAPIRequest } from '../../src/ui/provider-api';
5+
import { insertToFrontMatter, getFieldValues } from '../../src/ui/frontmatter';
6+
import { getPromptTemplate } from '../../src/domain/prompt';
77

88
// Mock dependencies
9-
vi.mock('../../src/provider', () => ({
9+
vi.mock('../../src/ui/provider-api', () => ({
1010
processAPIRequest: vi.fn(),
1111
}));
1212

13-
vi.mock('../../src/provider/prompt', () => ({
13+
vi.mock('../../src/domain/prompt', () => ({
1414
DEFAULT_SYSTEM_ROLE: 'Test system role',
1515
getPromptTemplate: vi.fn().mockReturnValue('Test prompt'),
1616
}));
1717

18-
vi.mock('../../src/lib/frontmatter', () => ({
18+
vi.mock('../../src/ui/frontmatter', () => ({
1919
getContentWithoutFrontmatter: vi.fn().mockReturnValue('Test content'),
2020
getFieldValues: vi.fn().mockReturnValue(['tag1', 'tag2']),
2121
insertToFrontMatter: vi.fn().mockResolvedValue(undefined),
2222
}));
2323

24-
vi.mock('../../src/settings/components/Notice', () => ({
24+
vi.mock('../../src/ui/settings/components/Notice', () => ({
2525
Notice: {
2626
error: vi.fn(),
2727
success: vi.fn(),

__tests__/main.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import AutoClassifierPlugin from 'main';
22
import { App, TFile, createMockTFile } from 'obsidian';
3-
import { DEFAULT_SETTINGS, DEFAULT_FRONTMATTER_SETTING } from '../src/constants';
4-
import type { ProviderConfig, FrontmatterField, OAuthTokens } from 'types';
3+
import { DEFAULT_SETTINGS, DEFAULT_FRONTMATTER_SETTING } from '../src/domain/constants';
4+
import type { ProviderConfig, FrontmatterField, OAuthTokens } from '../src/types/index';
55
import { Notice as SettingsNotice } from 'settings/components/Notice';
66
import { processAPIRequest } from 'provider';
77
import { insertToFrontMatter, getFieldValues } from 'lib/frontmatter';

__tests__/provider/auth/oauth.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { vi, describe, it, expect, beforeEach } from 'vitest';
22
import { Platform, requestUrl } from 'obsidian';
3-
import { CodexOAuth } from '../../../src/provider/auth/oauth';
4-
import type { OAuthTokens } from '../../../src/provider/auth/types';
5-
import { OAuthCallbackServer } from '../../../src/provider/auth/oauth-server';
6-
import { createTokensFromResponse, isTokenExpired } from '../../../src/provider/auth/token-manager';
3+
import { CodexOAuth } from '../../../src/ui/auth/oauth';
4+
import type { OAuthTokens } from '../../../src/types/auth';
5+
import { OAuthCallbackServer } from '../../../src/domain/auth/oauth-server';
6+
import { createTokensFromResponse, isTokenExpired } from '../../../src/domain/auth/token-manager';
77
import type { Mock } from 'vitest';
88

99
// Mock obsidian
@@ -15,7 +15,7 @@ vi.mock('obsidian', () => ({
1515
}));
1616

1717
// Mock the oauth-server - use a class-like constructor mock
18-
vi.mock('../../../src/provider/auth/oauth-server', () => {
18+
vi.mock('../../../src/domain/auth/oauth-server', () => {
1919
const MockServer = vi.fn(function (this: any) {
2020
this.waitForCallback = vi.fn();
2121
this.stop = vi.fn();
@@ -24,7 +24,7 @@ vi.mock('../../../src/provider/auth/oauth-server', () => {
2424
});
2525

2626
// Mock pkce
27-
vi.mock('../../../src/provider/auth/pkce', () => ({
27+
vi.mock('../../../src/domain/auth/pkce', () => ({
2828
generatePKCEChallenge: vi.fn().mockResolvedValue({
2929
codeVerifier: 'test-verifier',
3030
codeChallenge: 'test-challenge',
@@ -33,7 +33,7 @@ vi.mock('../../../src/provider/auth/pkce', () => ({
3333
}));
3434

3535
// Mock token-manager
36-
vi.mock('../../../src/provider/auth/token-manager', () => ({
36+
vi.mock('../../../src/domain/auth/token-manager', () => ({
3737
createTokensFromResponse: vi.fn().mockReturnValue({
3838
accessToken: 'new-access-token',
3939
refreshToken: 'new-refresh-token',

__tests__/provider/auth/pkce.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
generateCodeChallenge,
44
generateState,
55
generatePKCEChallenge,
6-
} from '../../../src/provider/auth/pkce';
6+
} from '../../../src/domain/auth/pkce';
77

88
describe('pkce', () => {
99
describe('generateCodeVerifier', () => {

__tests__/provider/auth/token-manager.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {
55
createTokensFromResponse,
66
getTokenRemainingTime,
77
formatTokenExpiry,
8-
} from '../../../src/provider/auth/token-manager';
9-
import type { OAuthTokens, TokenResponse } from '../../../src/provider/auth/types';
8+
} from '../../../src/domain/auth/token-manager';
9+
import type { OAuthTokens, TokenResponse } from '../../../src/types/auth';
1010

1111
describe('token-manager', () => {
1212
// Mock Date.now for consistent testing

__tests__/settings/ProviderSection.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type { Mock } from 'vitest';
2-
import { ProviderSection } from '../../src/settings/ProviderSection';
2+
import { ProviderSection } from '../../src/ui/settings/ProviderSection';
33
import type { App } from 'obsidian';
44
import type AutoClassifierPlugin from '../../src/main';
55
import type { ProviderConfig, OAuthTokens } from '../../src/types';
6-
import { formatTokenExpiry, isTokenExpired } from '../../src/provider/auth';
7-
import { Setting } from '../../src/settings/components/Setting';
6+
import { formatTokenExpiry, isTokenExpired } from '../../src/ui/auth';
7+
import { Setting } from '../../src/ui/settings/components/Setting';
88

99
// Mock the provider/auth module
10-
vi.mock('../../src/provider/auth', () => ({
10+
vi.mock('../../src/ui/auth', () => ({
1111
formatTokenExpiry: vi.fn((tokens) => {
1212
const remaining = tokens.expiresAt - Math.floor(Date.now() / 1000);
1313
if (remaining <= 0) return 'Expired';
@@ -22,14 +22,14 @@ vi.mock('../../src/provider/auth', () => ({
2222
}));
2323

2424
// Mock the ProviderModal
25-
vi.mock('../../src/settings/modals/ProviderModal', () => ({
25+
vi.mock('../../src/ui/settings/modals/ProviderModal', () => ({
2626
ProviderModal: vi.fn().mockImplementation(() => ({
2727
open: vi.fn(),
2828
})),
2929
}));
3030

3131
// Mock Setting component
32-
vi.mock('../../src/settings/components/Setting', () => ({
32+
vi.mock('../../src/ui/settings/components/Setting', () => ({
3333
Setting: {
3434
create: vi.fn(),
3535
},

__tests__/settings/TagSection.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ vi.mock('settings/modals/FrontmatterEditorModal', () => ({
55
import type { Mock } from 'vitest';
66
import type { FrontmatterField } from 'types';
77
import { Tag } from 'settings/TagSection';
8-
import { DEFAULT_TAG_SETTING } from '../../src/constants';
8+
import { DEFAULT_TAG_SETTING } from '../../src/domain/constants';
99

1010
interface MockPlugin {
1111
app: { vault: { getMarkdownFiles: Mock } };

eslint.base.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ export const baseConfig = tseslint.config(
2424
'no-console': 'error',
2525
},
2626
},
27+
{
28+
files: ['src/domain/**/*.ts', 'src/types/**/*.ts', 'src/utils/**/*.ts'],
29+
ignores: ['**/*.d.ts'],
30+
rules: {
31+
'no-restricted-imports': ['error', {
32+
patterns: [{
33+
group: ['obsidian', 'obsidian/*'],
34+
message: 'domain/, types/, and utils/ layers must not import from obsidian. Move this code to ui/ or inject the dependency.',
35+
}],
36+
}],
37+
},
38+
},
2739
{
2840
files: ['scripts/**/*.{js,mjs}', 'tooling/**/*.{js,mjs}', 'esbuild.config.mjs', 'version-bump.mjs'],
2941
languageOptions: {

eslint.config.mjs

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,51 +20,56 @@ export default [
2020
unicorn,
2121
},
2222
settings: {
23-
// Module boundary definitions
24-
// Structure: provider/ | classifier/ | settings/ | lib/
23+
// Module boundary definitions — 4-layer architecture
24+
// main → ui → domain → utils/types
2525
'boundaries/elements': [
2626
{
27-
type: 'provider',
28-
pattern: 'src/provider/**',
27+
type: 'main',
28+
pattern: 'src/main.ts',
29+
mode: 'file',
30+
},
31+
{
32+
type: 'ui',
33+
pattern: 'src/ui/**',
2934
mode: 'folder',
3035
},
3136
{
32-
type: 'classifier',
33-
pattern: 'src/classifier/**',
37+
type: 'domain',
38+
pattern: 'src/domain/**',
3439
mode: 'folder',
3540
},
3641
{
37-
type: 'settings',
38-
pattern: 'src/settings/**',
42+
type: 'utils',
43+
pattern: 'src/utils/**',
3944
mode: 'folder',
4045
},
4146
{
42-
type: 'lib',
43-
pattern: 'src/lib/**',
47+
type: 'types',
48+
pattern: 'src/types/**',
4449
mode: 'folder',
4550
},
4651
{
47-
type: 'main',
48-
pattern: 'src/main.ts',
49-
mode: 'file',
52+
type: 'shared',
53+
pattern: 'src/shared/**',
54+
mode: 'folder',
5055
},
5156
],
52-
// Dependency rules between modules
57+
// Dependency rules between layers
5358
'boundaries/rules': [
5459
{
55-
from: 'settings',
56-
disallow: ['provider'],
57-
message: 'settings cannot import provider directly. Use classifier instead.',
60+
from: 'domain',
61+
disallow: ['ui'],
62+
message: 'domain layer must not import from ui layer.',
5863
},
5964
{
60-
from: 'provider',
61-
disallow: ['settings', 'classifier'],
62-
message: 'provider is pure API layer. No UI/business logic dependency.',
65+
from: 'utils',
66+
disallow: ['ui', 'domain'],
67+
message: 'utils layer must not import from ui or domain.',
6368
},
6469
{
65-
from: 'lib',
66-
disallow: ['provider', 'classifier', 'settings'],
67-
message: 'lib is pure utility. No domain module dependency.',
70+
from: 'types',
71+
disallow: ['ui', 'domain', 'utils'],
72+
message: 'types layer must not import from other layers.',
6873
},
6974
],
7075
},

src/domain/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)