refactor: strip Phase/Plan/R-number references from source comments

This commit is contained in:
2026-05-13 09:51:45 +02:00
parent 075b729266
commit 17ae157365
66 changed files with 980 additions and 647 deletions

View File

@@ -3,89 +3,86 @@ import { defineContractSuite } from "@repo/core-testing/contract";
import type { IUsersRepository } from "../application/repositories/users.repository.interface.js";
import { userFactory } from "../__factories__/user.factory.js";
export const usersRepositoryContract =
defineContractSuite<IUsersRepository>(
"IUsersRepository",
({ buildSubject, getTracer }) => {
let repo: IUsersRepository;
export const usersRepositoryContract = defineContractSuite<IUsersRepository>(
"IUsersRepository",
({ buildSubject, getTracer }) => {
let repo: IUsersRepository;
beforeEach(async () => {
userFactory.reset();
repo = await buildSubject();
beforeEach(async () => {
userFactory.reset();
repo = await buildSubject();
});
// --- getUser ---
it("createUser then getUser returns it by id", async () => {
const seed = userFactory.build();
await repo.createUser(seed);
const result = await repo.getUser(seed.id);
expect(result?.id).toBe(seed.id);
expect(result?.username).toBe(seed.username);
});
it("getUser returns undefined for missing id", async () => {
expect(await repo.getUser("does-not-exist")).toBeUndefined();
});
// --- getUserByUsername ---
it("createUser then getUserByUsername returns it by username", async () => {
const seed = userFactory.build({ username: "alice" });
await repo.createUser(seed);
const result = await repo.getUserByUsername("alice");
expect(result?.id).toBe(seed.id);
expect(result?.username).toBe("alice");
});
it("getUserByUsername returns undefined for missing username", async () => {
expect(await repo.getUserByUsername("no-such-user")).toBeUndefined();
});
// --- createUser ---
it("createUser returns the created user", async () => {
const seed = userFactory.build({ username: "carol" });
const created = await repo.createUser(seed);
expect(created.id).toBe(seed.id);
expect(created.username).toBe("carol");
});
describe("span emission", () => {
it("getUser emits users.getUser span with id attribute", async () => {
if (!getTracer) return;
const tracer = getTracer();
tracer.reset();
await repo.getUser("nonexistent");
const span = tracer.findSpan("users.getUser");
expect(span).toBeDefined();
expect(span!.op).toBe("repository");
expect(span!.attributes.id).toBe("nonexistent");
});
// --- getUser ---
it("getUserByUsername emits users.getUserByUsername span", async () => {
if (!getTracer) return;
const tracer = getTracer();
tracer.reset();
await repo.getUserByUsername("alice");
const span = tracer.findSpan("users.getUserByUsername");
expect(span).toBeDefined();
expect(span!.op).toBe("repository");
});
it("createUser then getUser returns it by id", async () => {
const seed = userFactory.build();
it("createUser emits users.createUser span with id attribute", async () => {
if (!getTracer) return;
const tracer = getTracer();
tracer.reset();
const seed = userFactory.build({ username: "span-test-user" });
await repo.createUser(seed);
const result = await repo.getUser(seed.id);
expect(result?.id).toBe(seed.id);
expect(result?.username).toBe(seed.username);
const span = tracer.findSpan("users.createUser");
expect(span).toBeDefined();
expect(span!.op).toBe("repository");
expect(span!.attributes.id).toBe(seed.id);
});
it("getUser returns undefined for missing id", async () => {
expect(await repo.getUser("does-not-exist")).toBeUndefined();
});
// --- getUserByUsername ---
it("createUser then getUserByUsername returns it by username", async () => {
const seed = userFactory.build({ username: "alice" });
await repo.createUser(seed);
const result = await repo.getUserByUsername("alice");
expect(result?.id).toBe(seed.id);
expect(result?.username).toBe("alice");
});
it("getUserByUsername returns undefined for missing username", async () => {
expect(
await repo.getUserByUsername("no-such-user"),
).toBeUndefined();
});
// --- createUser ---
it("createUser returns the created user", async () => {
const seed = userFactory.build({ username: "carol" });
const created = await repo.createUser(seed);
expect(created.id).toBe(seed.id);
expect(created.username).toBe("carol");
});
describe("span emission (R50)", () => {
it("getUser emits users.getUser span with id attribute", async () => {
if (!getTracer) return;
const tracer = getTracer();
tracer.reset();
await repo.getUser("nonexistent");
const span = tracer.findSpan("users.getUser");
expect(span).toBeDefined();
expect(span!.op).toBe("repository");
expect(span!.attributes.id).toBe("nonexistent");
});
it("getUserByUsername emits users.getUserByUsername span", async () => {
if (!getTracer) return;
const tracer = getTracer();
tracer.reset();
await repo.getUserByUsername("alice");
const span = tracer.findSpan("users.getUserByUsername");
expect(span).toBeDefined();
expect(span!.op).toBe("repository");
});
it("createUser emits users.createUser span with id attribute", async () => {
if (!getTracer) return;
const tracer = getTracer();
tracer.reset();
const seed = userFactory.build({ username: "span-test-user" });
await repo.createUser(seed);
const span = tracer.findSpan("users.createUser");
expect(span).toBeDefined();
expect(span!.op).toBe("repository");
expect(span!.attributes.id).toBe(seed.id);
});
});
},
);
});
},
);

View File

@@ -1,6 +1,9 @@
import { describe, it, expect } from "vitest";
import { ZodError } from "zod";
import { signInUseCase, signInOutputSchema } from "@/application/use-cases/sign-in.use-case";
import {
signInUseCase,
signInOutputSchema,
} from "@/application/use-cases/sign-in.use-case";
import { MockUsersRepository } from "@/infrastructure/repositories/users.repository.mock";
import { MockAuthenticationService } from "@/infrastructure/services/authentication.service.mock";
import { AuthenticationError } from "@/entities/errors/auth";
@@ -18,7 +21,10 @@ describe("signInUseCase", () => {
await users.createUser(seedUser);
const useCase = signInUseCase(users, auth);
const result = await useCase({ username: "alice", password: "testpassword" });
const result = await useCase({
username: "alice",
password: "testpassword",
});
expect(result.session.userId).toBe(seedUser.id);
expect(result.cookie.name).toBe("session");
@@ -38,7 +44,10 @@ describe("signInUseCase", () => {
const users = new MockUsersRepository([]);
const auth = new MockAuthenticationService(users);
await users.createUser(
userFactory.build({ username: "alice", passwordHash: "hashed_correctpassword" }),
userFactory.build({
username: "alice",
passwordHash: "hashed_correctpassword",
}),
);
const useCase = signInUseCase(users, auth);
@@ -48,7 +57,7 @@ describe("signInUseCase", () => {
});
});
describe("signInUseCase output validation (R25)", () => {
describe("signInUseCase output validation", () => {
it("throws when authenticationService returns a malformed session", async () => {
const users = new MockUsersRepository([]);
const seed = userFactory.build({ username: "alice" });
@@ -61,7 +70,9 @@ describe("signInUseCase output validation (R25)", () => {
} as unknown as IAuthenticationService;
const useCase = signInUseCase(users, auth);
await expect(useCase({ username: "alice", password: "x" })).rejects.toBeInstanceOf(ZodError);
await expect(
useCase({ username: "alice", password: "x" }),
).rejects.toBeInstanceOf(ZodError);
});
it("exports an output schema that mirrors the success shape", () => {

View File

@@ -1,7 +1,10 @@
import { describe, it, expect } from "vitest";
import { ZodError } from "zod";
import { RecordingEventBus } from "@repo/core-testing/instrumentation";
import { signUpUseCase, signUpOutputSchema } from "@/application/use-cases/sign-up.use-case";
import {
signUpUseCase,
signUpOutputSchema,
} from "@/application/use-cases/sign-up.use-case";
import { MockUsersRepository } from "@/infrastructure/repositories/users.repository.mock";
import { MockAuthenticationService } from "@/infrastructure/services/authentication.service.mock";
import { AuthenticationError } from "@/entities/errors/auth";
@@ -33,7 +36,11 @@ describe("signUpUseCase", () => {
const useCase = signUpUseCase(users, auth, bus);
await expect(
useCase({ username: "alice", password: "secret_password", confirmPassword: "secret_password" }),
useCase({
username: "alice",
password: "secret_password",
confirmPassword: "secret_password",
}),
).rejects.toBeInstanceOf(AuthenticationError);
});
@@ -83,13 +90,17 @@ describe("signUpUseCase", () => {
const useCase = signUpUseCase(users, auth, bus);
await expect(
useCase({ username: "eve", password: "secret_password", confirmPassword: "secret_password" }),
useCase({
username: "eve",
password: "secret_password",
confirmPassword: "secret_password",
}),
).rejects.toBeInstanceOf(AuthenticationError);
expect(bus.published).toHaveLength(0);
});
});
describe("signUpUseCase output validation (R25)", () => {
describe("signUpUseCase output validation", () => {
it("throws when authenticationService returns a malformed session", async () => {
const users = new MockUsersRepository([]);
const auth = {
@@ -103,7 +114,11 @@ describe("signUpUseCase output validation (R25)", () => {
const bus = new RecordingEventBus();
const useCase = signUpUseCase(users, auth, bus);
await expect(
useCase({ username: "carol", password: "secret_password", confirmPassword: "secret_password" }),
useCase({
username: "carol",
password: "secret_password",
confirmPassword: "secret_password",
}),
).rejects.toBeInstanceOf(ZodError);
});

View File

@@ -39,12 +39,16 @@ export const signUpUseCase =
bus: EventBusProtocol | undefined,
) =>
async (input: SignUpInput): Promise<SignUpOutput> => {
const existingUser = await usersRepository.getUserByUsername(input.username);
const existingUser = await usersRepository.getUserByUsername(
input.username,
);
if (existingUser) {
throw new AuthenticationError("Username taken");
}
const passwordHash = await authenticationService.hashPassword(input.password);
const passwordHash = await authenticationService.hashPassword(
input.password,
);
const userId = authenticationService.generateUserId();
const newUser = await usersRepository.createUser({
@@ -53,11 +57,12 @@ export const signUpUseCase =
passwordHash,
});
const { cookie, session } = await authenticationService.createSession(newUser);
const { cookie, session } =
await authenticationService.createSession(newUser);
// Auth is username-based — synthesize a deterministic email so the event
// payload validates against userSignedUpEventSchema.email().
// bus is optional: absent when core-events is not wired (Phase 3+).
// bus is optional: absent when core-events is not wired.
if (bus) {
await bus.publish(userSignedUpEvent, {
userId: newUser.id,

View File

@@ -44,8 +44,12 @@ export async function bindDevSeedAuth(ctx: BindContext): Promise<void> {
if (authContainer.isBound(INSTRUMENTATION_SYMBOLS.LOGGER)) {
authContainer.unbind(INSTRUMENTATION_SYMBOLS.LOGGER);
}
authContainer.bind<ITracer>(INSTRUMENTATION_SYMBOLS.TRACER).toConstantValue(tracer);
authContainer.bind<ILogger>(INSTRUMENTATION_SYMBOLS.LOGGER).toConstantValue(logger);
authContainer
.bind<ITracer>(INSTRUMENTATION_SYMBOLS.TRACER)
.toConstantValue(tracer);
authContainer
.bind<ILogger>(INSTRUMENTATION_SYMBOLS.LOGGER)
.toConstantValue(logger);
if (authContainer.isBound(AUTH_SYMBOLS.IUsersRepository)) {
authContainer.unbind(AUTH_SYMBOLS.IUsersRepository);
@@ -61,7 +65,9 @@ export async function bindDevSeedAuth(ctx: BindContext): Promise<void> {
.toConstantValue(repo);
// Need auth service from container for use cases
const authService = authContainer.get<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService);
const authService = authContainer.get<IAuthenticationService>(
AUTH_SYMBOLS.IAuthenticationService,
);
// Wrap use cases + controllers identically to bind-production
const wrappedSignIn = withSpan(
@@ -102,9 +108,15 @@ export async function bindDevSeedAuth(ctx: BindContext): Promise<void> {
]) {
if (authContainer.isBound(sym)) authContainer.unbind(sym);
}
authContainer.bind(AUTH_SYMBOLS.ISignInUseCase).toConstantValue(wrappedSignIn);
authContainer.bind(AUTH_SYMBOLS.ISignUpUseCase).toConstantValue(wrappedSignUp);
authContainer.bind(AUTH_SYMBOLS.ISignOutUseCase).toConstantValue(wrappedSignOut);
authContainer
.bind(AUTH_SYMBOLS.ISignInUseCase)
.toConstantValue(wrappedSignIn);
authContainer
.bind(AUTH_SYMBOLS.ISignUpUseCase)
.toConstantValue(wrappedSignUp);
authContainer
.bind(AUTH_SYMBOLS.ISignOutUseCase)
.toConstantValue(wrappedSignOut);
authContainer
.bind(AUTH_SYMBOLS.ISignInController)
@@ -145,8 +157,7 @@ export async function bindDevSeedAuth(ctx: BindContext): Promise<void> {
),
),
);
// bus + queue are accept-and-forward in Phase 6; consumed by Phase 7 generator
// output at the <gen:event-handlers> / <gen:jobs> anchors below.
// bus + queue are passed through; generated handlers consume them at the anchors below.
void bus;
void queue;
void realtime;

View File

@@ -33,7 +33,8 @@ export function bindProductionAuth(ctx: BindProductionContext): void {
if (bound) return;
bound = true;
const { config, tracer, logger, bus, queue, realtime, realtimeRegistry } = ctx;
const { config, tracer, logger, bus, queue, realtime, realtimeRegistry } =
ctx;
// Bind shared instrumentation into feature container
if (authContainer.isBound(INSTRUMENTATION_SYMBOLS.TRACER)) {
@@ -42,15 +43,21 @@ export function bindProductionAuth(ctx: BindProductionContext): void {
if (authContainer.isBound(INSTRUMENTATION_SYMBOLS.LOGGER)) {
authContainer.unbind(INSTRUMENTATION_SYMBOLS.LOGGER);
}
authContainer.bind<ITracer>(INSTRUMENTATION_SYMBOLS.TRACER).toConstantValue(tracer);
authContainer.bind<ILogger>(INSTRUMENTATION_SYMBOLS.LOGGER).toConstantValue(logger);
authContainer
.bind<ITracer>(INSTRUMENTATION_SYMBOLS.TRACER)
.toConstantValue(tracer);
authContainer
.bind<ILogger>(INSTRUMENTATION_SYMBOLS.LOGGER)
.toConstantValue(logger);
// Real repositories
if (authContainer.isBound(AUTH_SYMBOLS.IUsersRepository)) {
authContainer.unbind(AUTH_SYMBOLS.IUsersRepository);
}
const repo = new UsersRepository(config, tracer, logger);
authContainer.bind<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository).toConstantValue(repo);
authContainer
.bind<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository)
.toConstantValue(repo);
if (authContainer.isBound(AUTH_SYMBOLS.IAuthenticationService)) {
authContainer.unbind(AUTH_SYMBOLS.IAuthenticationService);
@@ -100,9 +107,15 @@ export function bindProductionAuth(ctx: BindProductionContext): void {
]) {
if (authContainer.isBound(sym)) authContainer.unbind(sym);
}
authContainer.bind(AUTH_SYMBOLS.ISignInUseCase).toConstantValue(wrappedSignIn);
authContainer.bind(AUTH_SYMBOLS.ISignUpUseCase).toConstantValue(wrappedSignUp);
authContainer.bind(AUTH_SYMBOLS.ISignOutUseCase).toConstantValue(wrappedSignOut);
authContainer
.bind(AUTH_SYMBOLS.ISignInUseCase)
.toConstantValue(wrappedSignIn);
authContainer
.bind(AUTH_SYMBOLS.ISignUpUseCase)
.toConstantValue(wrappedSignUp);
authContainer
.bind(AUTH_SYMBOLS.ISignOutUseCase)
.toConstantValue(wrappedSignOut);
// Controllers — wrapped with span at bind time
for (const sym of [
@@ -151,8 +164,7 @@ export function bindProductionAuth(ctx: BindProductionContext): void {
),
),
);
// bus + queue are accept-and-forward in Phase 6; consumed by Phase 7 generator
// output at the <gen:event-handlers> / <gen:jobs> anchors below.
// bus + queue are passed through; generated handlers consume them at the anchors below.
void bus;
void queue;
void realtime;

View File

@@ -10,7 +10,7 @@ export {
export { InputParseError } from "./entities/errors/common";
export { SESSION_COOKIE } from "./config";
// Use case schemas + types (Plan 9 R18)
// Use case schemas + types
export {
signInInputSchema,
signInOutputSchema,
@@ -44,7 +44,6 @@ export {
} from "./events/user-signed-up.event";
// <gen:realtime-channels>
// Feature conformance manifest (added in conformance milestone i, exposed
// here in milestone ii so the boot-time assertion and future tooling can
// read the contract from the package boundary).
// Feature conformance manifest — declares this feature's use cases, audits,
// publishes, and consumes. Read by the boot-time assertion + ESLint rules.
export { authManifest, type AuthManifest } from "./feature.manifest";

View File

@@ -1,9 +1,12 @@
import { describe, it, expect } from "vitest";
import { RecordingTracer, RecordingLogger } from "@repo/core-testing/instrumentation";
import {
RecordingTracer,
RecordingLogger,
} from "@repo/core-testing/instrumentation";
import { MockUsersRepository } from "@/infrastructure/repositories/users.repository.mock";
// Mock repo also wraps in spans (R42); easier to assert without booting Payload.
describe("MockUsersRepository emits spans (R42)", () => {
// Mock repo also wraps in spans; easier to assert without booting Payload.
describe("MockUsersRepository emits spans", () => {
it("getUser emits one span with op='repository'", async () => {
const tracer = new RecordingTracer();
const logger = new RecordingLogger();
@@ -26,13 +29,19 @@ describe("MockUsersRepository emits spans (R42)", () => {
);
await repo.getUserByUsername("alice");
expect(tracer.findSpan("users.getUserByUsername")).toBeDefined();
expect(tracer.findSpan("users.getUserByUsername")!.attributes.found).toBe(true);
expect(tracer.findSpan("users.getUserByUsername")!.attributes.found).toBe(
true,
);
});
it("createUser records created=true", async () => {
const tracer = new RecordingTracer();
const repo = new MockUsersRepository([], tracer);
await repo.createUser({ id: "u1", username: "charlie", passwordHash: "hash" });
await repo.createUser({
id: "u1",
username: "charlie",
passwordHash: "hash",
});
expect(tracer.findSpan("users.createUser")).toBeDefined();
expect(tracer.findSpan("users.createUser")!.attributes.created).toBe(true);
});

View File

@@ -13,7 +13,7 @@ import type { User } from "../../entities/models/user";
// generic session interface without deep integration with Payload's REST/local
// API and cookie infrastructure.
//
// TODO(lazar-conformance §7): Implement these three methods once the session
// TODO: Implement these three methods once the session
// cookie strategy is settled. Until then they throw NotImplementedError to
// keep the production-shaped file in place without silently no-oping.
//
@@ -21,7 +21,7 @@ import type { User } from "../../entities/models/user";
class NotImplementedError extends Error {
constructor(method: string) {
super(`NotImplemented: AuthenticationService.${method} — see refactor log §7`);
super(`NotImplemented: AuthenticationService.${method}`);
this.name = "NotImplementedError";
}
}
@@ -42,10 +42,17 @@ export class AuthenticationService implements IAuthenticationService {
async hashPassword(password: string): Promise<string> {
const salt = crypto.randomBytes(SALT_LENGTH).toString("hex");
const hash = await new Promise<string>((resolve, reject) => {
crypto.pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, DIGEST, (err, derivedKey) => {
if (err) reject(err);
else resolve(derivedKey.toString("hex"));
});
crypto.pbkdf2(
password,
salt,
ITERATIONS,
KEY_LENGTH,
DIGEST,
(err, derivedKey) => {
if (err) reject(err);
else resolve(derivedKey.toString("hex"));
},
);
});
return `${salt}${SEPARATOR}${hash}`;
}
@@ -56,10 +63,17 @@ export class AuthenticationService implements IAuthenticationService {
const salt = parts[0]!;
const expectedHash = parts[1]!;
const actualHash = await new Promise<string>((resolve, reject) => {
crypto.pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, DIGEST, (err, derivedKey) => {
if (err) reject(err);
else resolve(derivedKey.toString("hex"));
});
crypto.pbkdf2(
password,
salt,
ITERATIONS,
KEY_LENGTH,
DIGEST,
(err, derivedKey) => {
if (err) reject(err);
else resolve(derivedKey.toString("hex"));
},
);
});
return crypto.timingSafeEqual(
Buffer.from(expectedHash, "hex"),
@@ -67,23 +81,27 @@ export class AuthenticationService implements IAuthenticationService {
);
}
// TODO(lazar-conformance §7): Implement using Payload's local.login / JWT session issuance.
// TODO:Implement using Payload's local.login / JWT session issuance.
// Payload creates sessions via its REST auth endpoint; mapping that to a
// generic { session: Session; cookie: Cookie } shape requires understanding
// the JWT payload structure and the cookie name/attributes Payload uses.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async createSession(user: User): Promise<{ session: Session; cookie: Cookie }> {
async createSession(
user: User,
): Promise<{ session: Session; cookie: Cookie }> {
throw new NotImplementedError("createSession");
}
// TODO(lazar-conformance §7): Implement using Payload's JWT verify mechanism.
// TODO:Implement using Payload's JWT verify mechanism.
// Need to call Payload's local API to verify the token and retrieve the user.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async validateSession(sessionId: string): Promise<{ user: User; session: Session }> {
async validateSession(
sessionId: string,
): Promise<{ user: User; session: Session }> {
throw new NotImplementedError("validateSession");
}
// TODO(lazar-conformance §7): Implement by clearing the session token.
// TODO:Implement by clearing the session token.
// Payload does not have a server-side session store by default; invalidation
// is typically done client-side by clearing the cookie.
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@@ -29,7 +29,7 @@ describe("authRouter", () => {
});
});
describe("authRouter (R26 error mapping)", () => {
describe("authRouter error mapping", () => {
beforeEach(() => {
if (authContainer.isBound(AUTH_SYMBOLS.IUsersRepository)) {
authContainer.unbind(AUTH_SYMBOLS.IUsersRepository);
@@ -39,7 +39,9 @@ describe("authRouter (R26 error mapping)", () => {
}
const users = new MockUsersRepository();
const auth = new MockAuthenticationService(users);
authContainer.bind<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository).toConstantValue(users);
authContainer
.bind<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository)
.toConstantValue(users);
authContainer
.bind<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService)
.toConstantValue(auth);