feat: consolidate web auth and add Effect v4 Project primitives

Auth:
- Add useWebAuth hook as the single web-facing auth interface with four
  states (loading/unauthenticated/authenticated/inconsistent)
- Normalize getCurrentUser to return {id: tokenIdentifier, name, email}
- Rename requireOwnerId to requireAuthUserId across all backend callers
- Set expectAuth:true on ConvexReactClient singleton
- Migrate _app, _auth, user-menu, use-personal-organization to useWebAuth
- Remove unused @code/auth/server export (zero callers confirmed)

Primitives:
- Add packages/primitives/src/project.ts with Effect v4 Project domain:
  URL normalization, six canonical context kinds, strict remote decoding,
  context write decisioning with revision tracking, and no-op detection
- Define PublicGit and ProjectStore replaceable ports
- Define ProjectApplication service orchestrating domain+ports with
  deterministic error mapping
- Add comprehensive test suite (28 tests) with in-memory store and fake
  PublicGit covering normalization, seeds, idempotency, tenant isolation,
  last-write-wins, and typed error mapping
This commit is contained in:
-Puter
2026-07-23 18:04:02 +05:30
parent ab9426b8fc
commit 477e54240d
20 changed files with 1827 additions and 97 deletions

View File

@@ -6,6 +6,7 @@
"exports": {
".": "./src/index.ts",
"./agent-os": "./src/agent-os.ts",
"./project": "./src/project.ts",
"./signal": "./src/signal.ts"
},
"scripts": {

View File

@@ -1,3 +1,4 @@
// oxlint-disable-next-line no-barrel-file -- The package root intentionally exposes its public modules.
export * from "./agent-os";
export * from "./project";
export * from "./signal";

View File

@@ -0,0 +1,857 @@
import { Effect, Layer } from "effect";
import { describe, expect, it } from "vitest";
import {
CONTEXT_KINDS,
decideContextWrite,
makeInitialContext,
preparePublicGitSource,
ProjectApplication,
ProjectApplicationError,
type ProjectImportOutcome,
ProjectDomain,
type ProjectView,
PublicGit,
PublicGitError,
type PublicGitImportResult,
ProjectStore,
type PublicTextResult,
type ContextDocumentState,
type ContextKind,
type ContextWrite,
type PreparedPublicGitSource,
} from "./project";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const expectError = async <A, E>(
effect: Effect.Effect<A, E>
): Promise<E> => {
try {
await Effect.runPromise(effect as never);
throw new Error("Expected failure but got success");
} catch (caught) {
if (caught instanceof Error && caught.message === "Expected failure but got success") {
throw caught;
}
return caught as E;
}
};
// ---------------------------------------------------------------------------
// In-memory ProjectStore
// ---------------------------------------------------------------------------
interface InMemoryProject {
readonly id: string;
readonly organizationId: string;
readonly name: string;
readonly source: {
readonly kind: "git";
readonly url: string;
readonly normalizedUrl: string;
readonly host: string;
readonly repositoryPath: string;
readonly defaultBranch?: string;
};
documents: Map<ContextKind, ContextDocumentState>;
createdAt: number;
updatedAt: number;
}
const decideWriteSync = (
existing: ContextDocumentState | undefined,
write: ContextWrite
): { document: ContextDocumentState; changed: boolean } => {
const pathMap: Record<ContextKind, string> = {
agents: "AGENTS.md",
business: "business.md",
design: "design.md",
product: "product.md",
readme: "README.md",
tech: "tech.md",
};
const origin =
write._tag === "Repository"
? "repository"
: write._tag === "PublicTextUrl"
? "public-url"
: write.origin;
const sourceUrl = write._tag === "UserText" ? undefined : write.sourceUrl;
if (
existing &&
existing.content === write.content &&
existing.origin === origin &&
existing.sourceUrl === sourceUrl
) {
return { changed: false, document: existing };
}
return {
changed: true,
document: {
content: write.content,
kind: write.kind,
origin,
path: pathMap[write.kind],
revision: (existing?.revision ?? 0) + 1,
sourceUrl,
},
};
};
const createInMemoryStoreLayer = () => {
const projects = new Map<string, InMemoryProject>();
const orgByUser = new Map<string, string>();
let nextId = 1;
const toView = (p: InMemoryProject): ProjectView => ({
contextDocuments: [...p.documents.values()],
createdAt: p.createdAt as never,
id: p.id as never,
name: p.name,
organizationId: p.organizationId as never,
sources: [
{
createdAt: p.createdAt as never,
defaultBranch: p.source.defaultBranch,
host: p.source.host,
kind: "git",
normalizedUrl: p.source.normalizedUrl,
projectId: p.id as never,
repositoryPath: p.source.repositoryPath,
updatedAt: p.updatedAt as never,
url: p.source.url,
},
],
updatedAt: p.updatedAt as never,
});
const toOutcome = (p: InMemoryProject): ProjectImportOutcome => ({
contextDocuments: [...p.documents.values()],
createdAt: p.createdAt as never,
id: p.id as never,
name: p.name,
organizationId: p.organizationId as never,
source: {
createdAt: p.createdAt as never,
defaultBranch: p.source.defaultBranch,
host: p.source.host,
kind: "git",
normalizedUrl: p.source.normalizedUrl,
projectId: p.id as never,
repositoryPath: p.source.repositoryPath,
updatedAt: p.updatedAt as never,
url: p.source.url,
},
updatedAt: p.updatedAt as never,
});
return Layer.succeed(
ProjectStore,
ProjectStore.of({
getCurrentOrganization: (userId) =>
Effect.succeed((orgByUser.get(userId) ?? `org-${userId}`) as never),
listProjects: (userId) =>
Effect.succeed(
(() => {
const orgId = orgByUser.get(userId);
return orgId
? [...projects.values()]
.filter((p) => p.organizationId === orgId)
.map(toView)
: ([] as readonly ProjectView[]);
})()
),
getProject: (userId, projectId) =>
Effect.succeed(
(() => {
const orgId = orgByUser.get(userId);
const project = projects.get(projectId as string);
if (!project || project.organizationId !== orgId) return null;
return toView(project);
})()
),
persistPublicGitImport: (input) =>
Effect.sync(() => {
const orgId = orgByUser.get(input.userId) ?? `org-${input.userId}`;
orgByUser.set(input.userId, orgId);
const existing = [...projects.values()].find(
(p) =>
p.organizationId === orgId &&
p.source.normalizedUrl === input.source.normalizedUrl
);
const now = Date.now();
if (existing) {
existing.updatedAt = now;
for (const doc of input.remote.documents) {
const result = decideWriteSync(existing.documents.get(doc.kind), {
_tag: "Repository" as const,
content: doc.content,
kind: doc.kind,
sourceUrl: input.source.url,
});
if (result.changed)
existing.documents.set(doc.kind, result.document);
}
return toOutcome(existing);
}
const id = `project-${nextId++}`;
const documents = new Map<ContextKind, ContextDocumentState>();
for (const seed of makeInitialContext(
input.source.projectName,
input.source.url
)) {
documents.set(seed.kind, seed);
}
for (const doc of input.remote.documents) {
const result = decideWriteSync(documents.get(doc.kind), {
_tag: "Repository" as const,
content: doc.content,
kind: doc.kind,
sourceUrl: input.source.url,
});
if (result.changed) documents.set(doc.kind, result.document);
}
const project: InMemoryProject = {
createdAt: now,
documents,
id,
name: input.source.projectName,
organizationId: orgId,
source: {
defaultBranch: input.remote.defaultBranch,
host: input.source.host,
kind: "git",
normalizedUrl: input.source.normalizedUrl,
repositoryPath: input.source.repositoryPath,
url: input.source.url,
},
updatedAt: now,
};
projects.set(id, project);
return toOutcome(project);
}),
putContext: (input) =>
Effect.sync(() => {
const orgId = orgByUser.get(input.userId);
const project = projects.get(input.projectId as string);
if (!project || project.organizationId !== orgId) {
throw new Error("Project not found");
}
const result = decideWriteSync(
project.documents.get(input.write.kind),
input.write
);
if (result.changed) {
project.documents.set(input.write.kind, result.document);
project.updatedAt = Date.now();
}
return { revision: result.document.revision };
}),
})
);
};
// ---------------------------------------------------------------------------
// Fake PublicGit layer
// ---------------------------------------------------------------------------
const makeFakePublicGitLayer = (
inspectImpl: (
source: PreparedPublicGitSource
) => PublicGitImportResult | PublicGitError,
fetchImpl: (url: string) => PublicTextResult | PublicGitError
) =>
Layer.succeed(
PublicGit,
PublicGit.of({
inspect: (source) =>
Effect.gen(function* () {
const result = inspectImpl(source);
if (result instanceof PublicGitError) {
return yield* Effect.fail(result);
}
return result;
}),
fetchText: (url) =>
Effect.gen(function* () {
const result = fetchImpl(url);
if (result instanceof PublicGitError) {
return yield* Effect.fail(result);
}
return result;
}),
})
);
const HELLO_WORLD_RESULT: PublicGitImportResult = {
defaultBranch: "main",
documents: [
{
kind: "readme",
path: "README.md",
content: "# Hello World\n\nA test repository.\n",
},
],
warnings: [],
};
// ---------------------------------------------------------------------------
// Test layer
// ---------------------------------------------------------------------------
const makeTestLayer = (
fakePublicGit: ReturnType<typeof makeFakePublicGitLayer>
) =>
ProjectApplication.layer.pipe(
Layer.provide(
Layer.mergeAll(ProjectDomain.layer, fakePublicGit, createInMemoryStoreLayer())
)
);
const runApp = <A, E>(
effect: Effect.Effect<A, E, ProjectApplication>,
layer: Layer.Layer<ProjectApplication>
): Promise<A> => Effect.runPromise(Effect.provide(effect, layer));
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("preparePublicGitSource", () => {
it("normalizes a GitHub URL with .git suffix", async () => {
const result = await Effect.runPromise(
preparePublicGitSource("https://github.com/octocat/Hello-World.git")
);
expect(result.host).toBe("github.com");
expect(result.repositoryPath).toBe("octocat/Hello-World");
expect(result.projectName).toBe("Hello-World");
expect(result.normalizedUrl).toBe("https://github.com/octocat/Hello-World");
});
it("normalizes a GitLab URL", async () => {
const result = await Effect.runPromise(
preparePublicGitSource("https://gitlab.com/gitlab-org/gitlab-test.git")
);
expect(result.host).toBe("gitlab.com");
expect(result.repositoryPath).toBe("gitlab-org/gitlab-test");
});
it("normalizes a Codeberg URL", async () => {
const result = await Effect.runPromise(
preparePublicGitSource("https://codeberg.org/Codeberg/Documentation.git")
);
expect(result.host).toBe("codeberg.org");
expect(result.repositoryPath).toBe("Codeberg/Documentation");
});
it("normalizes a generic HTTP(S) host without .git", async () => {
const result = await Effect.runPromise(
preparePublicGitSource("https://example.com/foo/bar")
);
expect(result.repositoryPath).toBe("foo/bar");
expect(result.projectName).toBe("bar");
});
it("rejects SSH URLs", async () => {
const error = await expectError(
preparePublicGitSource("git@github.com:octocat/Hello-World.git")
);
expect(error).toBeInstanceOf(ProjectApplicationError);
expect((error as ProjectApplicationError).reason).toBe(
"InvalidPublicGitUrl"
);
});
it("rejects URLs with credentials", async () => {
const error = await expectError(
preparePublicGitSource(
"https://user:pass@github.com/octocat/Hello-World.git"
)
);
expect((error as ProjectApplicationError).reason).toBe(
"InvalidPublicGitUrl"
);
});
it("rejects URLs with query strings", async () => {
const error = await expectError(
preparePublicGitSource(
"https://github.com/octocat/Hello-World.git?branch=dev"
)
);
expect((error as ProjectApplicationError).reason).toBe(
"InvalidPublicGitUrl"
);
});
it("rejects URLs with fragments", async () => {
const error = await expectError(
preparePublicGitSource("https://github.com/octocat/Hello-World.git#readme")
);
expect((error as ProjectApplicationError).reason).toBe(
"InvalidPublicGitUrl"
);
});
it("rejects root-only paths", async () => {
const error = await expectError(
preparePublicGitSource("https://github.com/")
);
expect((error as ProjectApplicationError).reason).toBe(
"InvalidPublicGitUrl"
);
});
});
describe("makeInitialContext", () => {
it("produces exactly six canonical seeds", () => {
const seeds = makeInitialContext("test-repo", "https://example.com/foo/bar");
expect(seeds).toHaveLength(6);
const kinds = seeds.map((s) => s.kind).sort();
expect(kinds).toEqual([...CONTEXT_KINDS].sort());
});
it("seeds README with project name and repository URL", () => {
const seeds = makeInitialContext("my-project", "https://example.com/foo/bar");
const readme = seeds.find((s) => s.kind === "readme")!;
expect(readme.content).toBe(
"# my-project\n\nRepository: https://example.com/foo/bar\n"
);
});
it("seeds placeholders with exact title and scaffold text", () => {
const seeds = makeInitialContext("test", "https://example.com/foo/bar");
const product = seeds.find((s) => s.kind === "product")!;
expect(product.content).toBe("# product.md\n\nContext not imported yet.\n");
});
it("all seeds start at revision 1", () => {
const seeds = makeInitialContext("test", "https://example.com/foo/bar");
for (const seed of seeds) {
expect(seed.revision).toBe(1);
}
});
});
describe("decideContextWrite", () => {
it("increments revision on content change", async () => {
const existing: ContextDocumentState = {
content: "old",
kind: "product",
origin: "paste",
path: "product.md",
revision: 1,
sourceUrl: undefined,
};
const result = await Effect.runPromise(
decideContextWrite({
existing,
write: {
_tag: "UserText",
content: "new content",
kind: "product",
origin: "paste",
},
})
);
expect(result.changed).toBe(true);
expect(result.document.revision).toBe(2);
expect(result.document.content).toBe("new content");
});
it("returns no-op when content/origin/source are identical", async () => {
const existing: ContextDocumentState = {
content: "same",
kind: "product",
origin: "paste",
path: "product.md",
revision: 5,
sourceUrl: undefined,
};
const result = await Effect.runPromise(
decideContextWrite({
existing,
write: {
_tag: "UserText",
content: "same",
kind: "product",
origin: "paste",
},
})
);
expect(result.changed).toBe(false);
expect(result.document).toBe(existing);
});
it("preserves exact text without trimming", async () => {
const result = await Effect.runPromise(
decideContextWrite({
existing: undefined,
write: {
_tag: "UserText",
content: " trailing spaces \n\n",
kind: "tech",
origin: "paste",
},
})
);
expect(result.document.content).toBe(" trailing spaces \n\n");
});
});
describe("ProjectApplication.importPublicGit", () => {
it("imports a repository with six canonical context documents", async () => {
const layer = makeTestLayer(
makeFakePublicGitLayer(
() => HELLO_WORLD_RESULT,
() => ({ content: "", finalUrl: "" })
)
);
const outcome = await runApp(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.importPublicGit({
userId: "user-a",
repositoryUrl: "https://github.com/octocat/Hello-World.git",
});
}),
layer
);
expect(outcome.name).toBe("Hello-World");
expect(outcome.source.host).toBe("github.com");
expect(outcome.contextDocuments).toHaveLength(6);
const readme = outcome.contextDocuments.find((d) => d.kind === "readme")!;
expect(readme.content).toBe("# Hello World\n\nA test repository.\n");
});
it("is idempotent on re-import without .git suffix", async () => {
const layer = makeTestLayer(
makeFakePublicGitLayer(
() => HELLO_WORLD_RESULT,
() => ({ content: "", finalUrl: "" })
)
);
const first = await runApp(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.importPublicGit({
userId: "user-a",
repositoryUrl: "https://github.com/octocat/Hello-World.git",
});
}),
layer
);
const second = await runApp(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.importPublicGit({
userId: "user-a",
repositoryUrl: "https://github.com/octocat/Hello-World",
});
}),
layer
);
expect(second.id).toBe(first.id);
});
it("rejects an invalid URL", async () => {
const layer = makeTestLayer(
makeFakePublicGitLayer(
() => HELLO_WORLD_RESULT,
() => ({ content: "", finalUrl: "" })
)
);
const error = await expectError(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.importPublicGit({
userId: "user-a",
repositoryUrl: "not-a-url",
});
}).pipe(Effect.provide(layer))
);
expect((error as ProjectApplicationError).reason).toBe(
"InvalidPublicGitUrl"
);
});
it("maps PublicGit unreachable error to PublicGitUnreachable", async () => {
const layer = makeTestLayer(
makeFakePublicGitLayer(
() =>
new PublicGitError({
message: "Connection refused",
reason: "Unreachable",
}),
() => ({ content: "", finalUrl: "" })
)
);
const error = await expectError(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.importPublicGit({
userId: "user-a",
repositoryUrl: "https://github.com/octocat/Hello-World.git",
});
}).pipe(Effect.provide(layer))
);
expect((error as ProjectApplicationError).reason).toBe(
"PublicGitUnreachable"
);
});
it("maps PublicGit cancelled error to PublicGitUnreachable", async () => {
const layer = makeTestLayer(
makeFakePublicGitLayer(
() =>
new PublicGitError({
message: "Timed out",
reason: "Cancelled",
}),
() => ({ content: "", finalUrl: "" })
)
);
const error = await expectError(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.importPublicGit({
userId: "user-a",
repositoryUrl: "https://github.com/octocat/Hello-World.git",
});
}).pipe(Effect.provide(layer))
);
expect((error as ProjectApplicationError).reason).toBe(
"PublicGitUnreachable"
);
});
});
describe("ProjectApplication.putContext", () => {
const setupWithProject = async () => {
const layer = makeTestLayer(
makeFakePublicGitLayer(
() => HELLO_WORLD_RESULT,
() => ({ content: "", finalUrl: "" })
)
);
const outcome = await runApp(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.importPublicGit({
userId: "user-a",
repositoryUrl: "https://github.com/octocat/Hello-World.git",
});
}),
layer
);
return { layer, projectId: outcome.id };
};
it("writes pasted context and increments revision", async () => {
const { layer, projectId } = await setupWithProject();
const result = await runApp(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.putContext({
userId: "user-a",
projectId,
kind: "design",
content: "# Design\n\nNew design notes.\n",
origin: "paste",
});
}),
layer
);
// Seeds start at revision 1; first explicit write bumps to 2
expect(result.revision).toBe(2);
});
it("returns the same revision for identical content (no-op)", async () => {
const { layer, projectId } = await setupWithProject();
await runApp(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.putContext({
userId: "user-a",
projectId,
kind: "design",
content: "same content",
origin: "paste",
});
}),
layer
);
const result = await runApp(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.putContext({
userId: "user-a",
projectId,
kind: "design",
content: "same content",
origin: "paste",
});
}),
layer
);
expect(result.revision).toBe(2);
});
it("last explicit write wins for the same kind", async () => {
const { layer, projectId } = await setupWithProject();
await runApp(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.putContext({
userId: "user-a",
projectId,
kind: "product",
content: "first paste",
origin: "paste",
});
}),
layer
);
const result = await runApp(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.putContext({
userId: "user-a",
projectId,
kind: "product",
content: "second upload",
origin: "upload",
});
}),
layer
);
expect(result.revision).toBe(3);
});
});
describe("ProjectApplication.importPublicText", () => {
it("imports text from a URL and increments revision", async () => {
const layer = makeTestLayer(
makeFakePublicGitLayer(
() => HELLO_WORLD_RESULT,
(url) => ({
content: "# Tech via URL\n\nImported.\n",
finalUrl: url,
})
)
);
const outcome = await runApp(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.importPublicGit({
userId: "user-a",
repositoryUrl: "https://github.com/octocat/Hello-World.git",
});
}),
layer
);
const result = await runApp(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.importPublicText({
userId: "user-a",
projectId: outcome.id,
kind: "tech",
url: "https://raw.githubusercontent.com/foo/bar/main/tech.md",
});
}),
layer
);
expect(result.revision).toBe(2);
});
it("rejects empty URL", async () => {
const layer = makeTestLayer(
makeFakePublicGitLayer(
() => HELLO_WORLD_RESULT,
() => ({ content: "text", finalUrl: "url" })
)
);
const error = await expectError(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.importPublicText({
userId: "user-a",
projectId: "project-1" as never,
kind: "tech",
url: " ",
});
}).pipe(Effect.provide(layer))
);
expect((error as ProjectApplicationError).reason).toBe(
"InvalidContextInput"
);
});
});
describe("ProjectApplication tenant isolation", () => {
it("a second tenant cannot list the first tenant's projects", async () => {
const layer = makeTestLayer(
makeFakePublicGitLayer(
() => HELLO_WORLD_RESULT,
() => ({ content: "", finalUrl: "" })
)
);
await runApp(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.importPublicGit({
userId: "user-a",
repositoryUrl: "https://github.com/octocat/Hello-World.git",
});
}),
layer
);
const secondProjects = await runApp(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.listProjects({ userId: "user-b" });
}),
layer
);
expect(secondProjects).toHaveLength(0);
});
it("a second tenant cannot read the first tenant's project", async () => {
const layer = makeTestLayer(
makeFakePublicGitLayer(
() => HELLO_WORLD_RESULT,
() => ({ content: "", finalUrl: "" })
)
);
const outcome = await runApp(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.importPublicGit({
userId: "user-a",
repositoryUrl: "https://github.com/octocat/Hello-World.git",
});
}),
layer
);
const result = await runApp(
Effect.gen(function* () {
const a = yield* ProjectApplication;
return yield* a.getProject({
userId: "user-b",
projectId: outcome.id,
});
}),
layer
);
expect(result).toBeNull();
});
});

View File

@@ -0,0 +1,754 @@
import { Context, Effect, Layer, Schema } from "effect";
import { OrganizationId, ProjectId, TimestampMs } from "./signal.js";
export type { OrganizationId, ProjectId } from "./signal.js";
// ---------------------------------------------------------------------------
// Domain constants
// ---------------------------------------------------------------------------
export const MAX_CONTEXT_DOCUMENT_CHARACTERS = 200_000;
export const MAX_CONTEXT_BATCH_CHARACTERS = 600_000;
export const CONTEXT_KINDS = [
"readme",
"agents",
"product",
"business",
"design",
"tech",
] as const;
export type ContextKind = (typeof CONTEXT_KINDS)[number];
const CONTEXT_KIND_TO_PATH: Readonly<Record<ContextKind, string>> = {
agents: "AGENTS.md",
business: "business.md",
design: "design.md",
product: "product.md",
readme: "README.md",
tech: "tech.md",
};
const PATH_TO_CONTEXT_KIND: Readonly<Record<string, ContextKind>> =
Object.fromEntries(
Object.entries(CONTEXT_KIND_TO_PATH).map(([kind, path]) => [
path,
kind as ContextKind,
])
);
export const contextPathForKind = (kind: ContextKind): string =>
CONTEXT_KIND_TO_PATH[kind];
export const contextKindForPath = (path: string): ContextKind | null =>
PATH_TO_CONTEXT_KIND[path] ?? null;
const ContextKindSchema = Schema.Literals([...CONTEXT_KINDS]);
const MeaningfulString = Schema.String.check(
Schema.makeFilter((value) => value.trim().length > 0, {
expected: "a non-empty string",
})
);
// ---------------------------------------------------------------------------
// Shared data shapes
// ---------------------------------------------------------------------------
export const PreparedPublicGitSource = Schema.Struct({
host: Schema.String,
projectName: MeaningfulString,
repositoryPath: MeaningfulString,
normalizedUrl: Schema.String,
url: Schema.String,
});
export type PreparedPublicGitSource = typeof PreparedPublicGitSource.Type;
export const RepositoryContextDocument = Schema.Struct({
kind: ContextKindSchema,
path: Schema.String,
content: Schema.String,
});
export type RepositoryContextDocument = typeof RepositoryContextDocument.Type;
export const ProjectImportWarning = Schema.Struct({
message: Schema.String,
path: Schema.String,
});
export type ProjectImportWarning = typeof ProjectImportWarning.Type;
export const PublicGitImportResult = Schema.Struct({
defaultBranch: Schema.UndefinedOr(Schema.String),
documents: Schema.Array(RepositoryContextDocument),
warnings: Schema.Array(ProjectImportWarning),
});
export type PublicGitImportResult = typeof PublicGitImportResult.Type;
export const PublicTextResult = Schema.Struct({
content: Schema.String,
finalUrl: Schema.String,
});
export type PublicTextResult = typeof PublicTextResult.Type;
export const ContextOrigin = Schema.Literals([
"repository",
"paste",
"upload",
"public-url",
]);
export type ContextOrigin = typeof ContextOrigin.Type;
export const ContextDocumentState = Schema.Struct({
content: Schema.String,
kind: ContextKindSchema,
origin: ContextOrigin,
path: Schema.String,
revision: Schema.Number,
sourceUrl: Schema.UndefinedOr(Schema.String),
});
export type ContextDocumentState = typeof ContextDocumentState.Type;
const RepositoryWrite = Schema.Struct({
_tag: Schema.Literal("Repository"),
content: Schema.String,
kind: ContextKindSchema,
sourceUrl: Schema.String,
});
const PublicTextUrlWrite = Schema.Struct({
_tag: Schema.Literal("PublicTextUrl"),
content: Schema.String,
kind: ContextKindSchema,
sourceUrl: Schema.String,
});
const UserTextWrite = Schema.Struct({
_tag: Schema.Literal("UserText"),
content: Schema.String,
kind: ContextKindSchema,
origin: Schema.Literals(["paste", "upload"]),
});
export const ContextWrite = Schema.Union([
RepositoryWrite,
PublicTextUrlWrite,
UserTextWrite,
]);
export type ContextWrite = typeof ContextWrite.Type;
export const ProjectSourceView = Schema.Struct({
createdAt: TimestampMs,
defaultBranch: Schema.UndefinedOr(Schema.String),
host: Schema.String,
kind: Schema.Literal("git"),
normalizedUrl: Schema.String,
projectId: ProjectId,
repositoryPath: Schema.String,
updatedAt: TimestampMs,
url: Schema.String,
});
export type ProjectSourceView = typeof ProjectSourceView.Type;
export const ProjectView = Schema.Struct({
contextDocuments: Schema.Array(ContextDocumentState),
createdAt: TimestampMs,
id: ProjectId,
name: Schema.String,
organizationId: OrganizationId,
sources: Schema.Array(ProjectSourceView),
updatedAt: TimestampMs,
});
export type ProjectView = typeof ProjectView.Type;
export const ProjectImportOutcome = Schema.Struct({
contextDocuments: Schema.Array(ContextDocumentState),
createdAt: TimestampMs,
id: ProjectId,
name: Schema.String,
organizationId: OrganizationId,
source: ProjectSourceView,
updatedAt: TimestampMs,
});
export type ProjectImportOutcome = typeof ProjectImportOutcome.Type;
// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------
export const PublicGitErrorReason = Schema.Literals([
"InvalidUrl",
"Unreachable",
"InvalidResponse",
"Cancelled",
]);
export type PublicGitErrorReason = typeof PublicGitErrorReason.Type;
export class PublicGitError extends Schema.TaggedErrorClass<PublicGitError>()(
"PublicGitError",
{
message: Schema.String,
reason: PublicGitErrorReason,
}
) {}
export const ProjectStoreErrorReason = Schema.Literals([
"NotFound",
"Forbidden",
"Conflict",
"Persistence",
]);
export type ProjectStoreErrorReason = typeof ProjectStoreErrorReason.Type;
export class ProjectStoreError extends Schema.TaggedErrorClass<ProjectStoreError>()(
"ProjectStoreError",
{
message: Schema.String,
reason: ProjectStoreErrorReason,
}
) {}
export const ProjectApplicationErrorReason = Schema.Literals([
"Authentication",
"InvalidPublicGitUrl",
"PublicGitUnreachable",
"InvalidRemoteResult",
"InvalidContextInput",
"ContextDocumentTooLarge",
"ContextBatchTooLarge",
"ProjectNotFound",
"Persistence",
]);
export type ProjectApplicationErrorReason =
typeof ProjectApplicationErrorReason.Type;
export class ProjectApplicationError extends Schema.TaggedErrorClass<ProjectApplicationError>()(
"ProjectApplicationError",
{
message: Schema.String,
reason: ProjectApplicationErrorReason,
}
) {}
// ---------------------------------------------------------------------------
// Pure domain functions
// ---------------------------------------------------------------------------
const isWhitespaceOnly = (value: string): boolean => value.trim().length === 0;
const INVALID_GIT_URL = new ProjectApplicationError({
message: "Use a public http(s) Git repository URL",
reason: "InvalidPublicGitUrl",
});
const INVALID_REMOTE = new ProjectApplicationError({
message: "Invalid remote result",
reason: "InvalidRemoteResult",
});
export const preparePublicGitSource = (
rawUrl: string
): Effect.Effect<PreparedPublicGitSource, ProjectApplicationError> =>
Effect.gen(function* () {
let parsed: URL;
try {
parsed = new URL(rawUrl);
} catch {
return yield* Effect.fail(INVALID_GIT_URL);
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
return yield* Effect.fail(INVALID_GIT_URL);
}
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
return yield* Effect.fail(INVALID_GIT_URL);
}
const host = parsed.hostname.toLowerCase();
let repositoryPath = parsed.pathname;
if (repositoryPath.startsWith("/")) {
repositoryPath = repositoryPath.slice(1);
}
if (repositoryPath.endsWith("/")) {
repositoryPath = repositoryPath.slice(0, -1);
}
if (repositoryPath.endsWith(".git")) {
repositoryPath = repositoryPath.slice(0, -4);
}
if (repositoryPath.length === 0) {
return yield* Effect.fail(INVALID_GIT_URL);
}
const segments = repositoryPath.split("/").filter((s) => s.length > 0);
const lastSegment = segments[segments.length - 1];
if (!lastSegment || isWhitespaceOnly(lastSegment)) {
return yield* Effect.fail(INVALID_GIT_URL);
}
const projectName = lastSegment;
const normalizedUrl = `${parsed.protocol}//${host}/${repositoryPath}`;
return {
host,
normalizedUrl,
projectName,
repositoryPath,
url: normalizedUrl,
};
});
export const decodePublicGitImportResult = (
raw: unknown
): Effect.Effect<PublicGitImportResult, ProjectApplicationError> =>
Effect.gen(function* () {
const result = yield* Schema.decodeUnknownEffect(PublicGitImportResult)(
raw
).pipe(Effect.mapError(() => INVALID_REMOTE));
const seenKinds = new Set<string>();
const seenPaths = new Set<string>();
let totalChars = 0;
for (const doc of result.documents) {
const canonicalPath = CONTEXT_KIND_TO_PATH[doc.kind];
if (doc.path !== canonicalPath) {
return yield* Effect.fail(INVALID_REMOTE);
}
if (seenKinds.has(doc.kind) || seenPaths.has(doc.path)) {
return yield* Effect.fail(INVALID_REMOTE);
}
if (isWhitespaceOnly(doc.content)) {
return yield* Effect.fail(INVALID_REMOTE);
}
if (doc.content.length > MAX_CONTEXT_DOCUMENT_CHARACTERS) {
return yield* Effect.fail(
new ProjectApplicationError({
message: "Context document exceeds 200000 characters",
reason: "ContextDocumentTooLarge",
})
);
}
seenKinds.add(doc.kind);
seenPaths.add(doc.path);
totalChars += doc.content.length;
}
if (totalChars > MAX_CONTEXT_BATCH_CHARACTERS) {
return yield* Effect.fail(
new ProjectApplicationError({
message: "Context batch exceeds 600000 characters",
reason: "ContextBatchTooLarge",
})
);
}
return result;
});
export const decodePublicTextResult = (
raw: unknown
): Effect.Effect<PublicTextResult, ProjectApplicationError> =>
Effect.gen(function* () {
const result = yield* Schema.decodeUnknownEffect(PublicTextResult)(
raw
).pipe(Effect.mapError(() => INVALID_REMOTE));
if (isWhitespaceOnly(result.content)) {
return yield* Effect.fail(
new ProjectApplicationError({
message: "Context document cannot be empty",
reason: "InvalidContextInput",
})
);
}
if (result.content.length > MAX_CONTEXT_DOCUMENT_CHARACTERS) {
return yield* Effect.fail(
new ProjectApplicationError({
message: "Context document exceeds 200000 characters",
reason: "ContextDocumentTooLarge",
})
);
}
if (isWhitespaceOnly(result.finalUrl)) {
return yield* Effect.fail(INVALID_REMOTE);
}
return result;
});
const resolveKindFromPath = (path: string): ContextKind => {
const kind = contextKindForPath(path);
if (kind === null) {
throw new Error(`Unknown canonical context path: ${path}`);
}
return kind;
};
export const makeInitialContext = (
projectName: string,
repositoryUrl: string
): readonly ContextDocumentState[] => {
const placeholder = (path: string): ContextDocumentState => ({
content: `# ${path}\n\nContext not imported yet.\n`,
kind: resolveKindFromPath(path),
origin: "repository",
path,
revision: 1,
sourceUrl: repositoryUrl,
});
return [
{
content: `# ${projectName}\n\nRepository: ${repositoryUrl}\n`,
kind: "readme",
origin: "repository",
path: "README.md",
revision: 1,
sourceUrl: repositoryUrl,
},
placeholder("AGENTS.md"),
placeholder("product.md"),
placeholder("business.md"),
placeholder("design.md"),
placeholder("tech.md"),
];
};
export interface DecideContextWriteInput {
readonly existing: ContextDocumentState | undefined;
readonly write: ContextWrite;
}
export interface DecideContextWriteResult {
readonly document: ContextDocumentState;
readonly changed: boolean;
}
export const decideContextWrite = (
input: DecideContextWriteInput
): Effect.Effect<DecideContextWriteResult, ProjectApplicationError> =>
Effect.gen(function* () {
const write = input.write;
const content = write.content;
const kind = write.kind;
const path = contextPathForKind(kind);
if (isWhitespaceOnly(content)) {
return yield* Effect.fail(
new ProjectApplicationError({
message: "Context document cannot be empty",
reason: "InvalidContextInput",
})
);
}
if (content.length > MAX_CONTEXT_DOCUMENT_CHARACTERS) {
return yield* Effect.fail(
new ProjectApplicationError({
message: "Context document exceeds 200000 characters",
reason: "ContextDocumentTooLarge",
})
);
}
const origin: ContextOrigin =
write._tag === "Repository"
? "repository"
: write._tag === "PublicTextUrl"
? "public-url"
: write.origin;
const sourceUrl: string | undefined =
write._tag === "UserText" ? undefined : write.sourceUrl;
const existing = input.existing;
if (
existing &&
existing.content === content &&
existing.origin === origin &&
existing.sourceUrl === sourceUrl
) {
return { changed: false, document: existing };
}
const nextRevision = (existing?.revision ?? 0) + 1;
return {
changed: true,
document: { content, kind, origin, path, revision: nextRevision, sourceUrl },
};
});
// ---------------------------------------------------------------------------
// ProjectDomain service
// ---------------------------------------------------------------------------
interface ProjectDomainShape {
readonly preparePublicGitSource: typeof preparePublicGitSource;
readonly decodePublicGitImportResult: typeof decodePublicGitImportResult;
readonly decodePublicTextResult: typeof decodePublicTextResult;
readonly makeInitialContext: typeof makeInitialContext;
readonly decideContextWrite: typeof decideContextWrite;
}
export class ProjectDomain extends Context.Service<
ProjectDomain,
ProjectDomainShape
>()("@code/primitives/project/ProjectDomain") {
static readonly layer = Layer.succeed(
ProjectDomain,
ProjectDomain.of({
preparePublicGitSource,
decodePublicGitImportResult,
decodePublicTextResult,
makeInitialContext,
decideContextWrite,
})
);
}
// ---------------------------------------------------------------------------
// PublicGit port
// ---------------------------------------------------------------------------
interface PublicGitShape {
readonly inspect: (
source: PreparedPublicGitSource
) => Effect.Effect<PublicGitImportResult, PublicGitError>;
readonly fetchText: (
url: string
) => Effect.Effect<PublicTextResult, PublicGitError>;
}
export class PublicGit extends Context.Service<PublicGit, PublicGitShape>()(
"@code/primitives/project/PublicGit"
) {}
// ---------------------------------------------------------------------------
// ProjectStore port
// ---------------------------------------------------------------------------
interface ProjectStoreShape {
readonly getCurrentOrganization: (
userId: string
) => Effect.Effect<OrganizationId, ProjectStoreError>;
readonly listProjects: (
userId: string
) => Effect.Effect<readonly ProjectView[], ProjectStoreError>;
readonly getProject: (
userId: string,
projectId: ProjectId
) => Effect.Effect<ProjectView | null, ProjectStoreError>;
readonly persistPublicGitImport: (input: {
readonly userId: string;
readonly source: PreparedPublicGitSource;
readonly remote: PublicGitImportResult;
}) => Effect.Effect<ProjectImportOutcome, ProjectStoreError>;
readonly putContext: (input: {
readonly userId: string;
readonly projectId: ProjectId;
readonly write: ContextWrite;
}) => Effect.Effect<{ readonly revision: number }, ProjectStoreError>;
}
export class ProjectStore extends Context.Service<
ProjectStore,
ProjectStoreShape
>()("@code/primitives/project/ProjectStore") {}
// ---------------------------------------------------------------------------
// Error mapping
// ---------------------------------------------------------------------------
const mapStoreError = (error: ProjectStoreError): ProjectApplicationError => {
if (error.reason === "NotFound" || error.reason === "Forbidden") {
return new ProjectApplicationError({
message: "Project not found",
reason: "ProjectNotFound",
});
}
return new ProjectApplicationError({
message: error.message,
reason: "Persistence",
});
};
const mapPublicGitError = (error: PublicGitError): ProjectApplicationError => {
if (error.reason === "InvalidUrl") {
return new ProjectApplicationError({
message: error.message,
reason: "InvalidPublicGitUrl",
});
}
if (error.reason === "Unreachable" || error.reason === "Cancelled") {
return new ProjectApplicationError({
message: error.message,
reason: "PublicGitUnreachable",
});
}
return new ProjectApplicationError({
message: error.message,
reason: "InvalidRemoteResult",
});
};
// ---------------------------------------------------------------------------
// ProjectApplication service
// ---------------------------------------------------------------------------
interface ProjectApplicationShape {
readonly importPublicGit: (input: {
readonly userId: string;
readonly repositoryUrl: unknown;
}) => Effect.Effect<ProjectImportOutcome, ProjectApplicationError>;
readonly importPublicText: (input: {
readonly userId: string;
readonly projectId: ProjectId;
readonly kind: ContextKind;
readonly url: string;
}) => Effect.Effect<{ readonly revision: number }, ProjectApplicationError>;
readonly putContext: (input: {
readonly userId: string;
readonly projectId: ProjectId;
readonly kind: ContextKind;
readonly content: string;
readonly origin: "paste" | "upload";
}) => Effect.Effect<{ readonly revision: number }, ProjectApplicationError>;
readonly listProjects: (input: {
readonly userId: string;
}) => Effect.Effect<readonly ProjectView[], ProjectApplicationError>;
readonly getProject: (input: {
readonly userId: string;
readonly projectId: ProjectId;
}) => Effect.Effect<ProjectView | null, ProjectApplicationError>;
}
export class ProjectApplication extends Context.Service<
ProjectApplication,
ProjectApplicationShape
>()("@code/primitives/project/ProjectApplication") {
static readonly layer = Layer.effect(
ProjectApplication,
Effect.gen(function* () {
const domain = yield* ProjectDomain;
const publicGit = yield* PublicGit;
const store = yield* ProjectStore;
return ProjectApplication.of({
importPublicGit: Effect.fn("ProjectApplication.importPublicGit")(
function* ({
userId,
repositoryUrl,
}: {
readonly userId: string;
readonly repositoryUrl: unknown;
}) {
if (typeof repositoryUrl !== "string") {
return yield* Effect.fail(INVALID_GIT_URL);
}
const source = yield* domain.preparePublicGitSource(
repositoryUrl
);
const rawRemote = yield* publicGit
.inspect(source)
.pipe(Effect.mapError(mapPublicGitError));
const remote = yield* domain.decodePublicGitImportResult(
rawRemote
);
return yield* store
.persistPublicGitImport({ userId, source, remote })
.pipe(Effect.mapError(mapStoreError));
}
),
importPublicText: Effect.fn("ProjectApplication.importPublicText")(
function* ({
userId,
projectId,
kind,
url,
}: {
readonly userId: string;
readonly projectId: ProjectId;
readonly kind: ContextKind;
readonly url: string;
}) {
if (isWhitespaceOnly(url)) {
return yield* Effect.fail(
new ProjectApplicationError({
message: "Use a public text URL",
reason: "InvalidContextInput",
})
);
}
const rawText = yield* publicGit
.fetchText(url)
.pipe(Effect.mapError(mapPublicGitError));
const text = yield* domain.decodePublicTextResult(rawText);
const write: ContextWrite = {
_tag: "PublicTextUrl",
content: text.content,
kind,
sourceUrl: text.finalUrl,
};
return yield* store
.putContext({ userId, projectId, write })
.pipe(Effect.mapError(mapStoreError));
}
),
putContext: Effect.fn("ProjectApplication.putContext")(
function* ({
userId,
projectId,
kind,
content,
origin,
}: {
readonly userId: string;
readonly projectId: ProjectId;
readonly kind: ContextKind;
readonly content: string;
readonly origin: "paste" | "upload";
}) {
const write: ContextWrite = {
_tag: "UserText",
content,
kind,
origin,
};
return yield* store
.putContext({ userId, projectId, write })
.pipe(Effect.mapError(mapStoreError));
}
),
listProjects: Effect.fn("ProjectApplication.listProjects")(
function* ({ userId }: { readonly userId: string }) {
return yield* store
.listProjects(userId)
.pipe(Effect.mapError(mapStoreError));
}
),
getProject: Effect.fn("ProjectApplication.getProject")(
function* ({
userId,
projectId,
}: {
readonly userId: string;
readonly projectId: ProjectId;
}) {
return yield* store
.getProject(userId, projectId)
.pipe(Effect.mapError(mapStoreError));
}
),
});
})
);
}