Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to CorpusKit will be documented here. The project follows

## [Unreleased]

### Added

- Append-only corpus version creation from manual sentences or bounded UTF-8 TXT, CSV, and JSON
imports, with parent lineage, atomic quota accounting, audit evidence, API contracts, and an
accessible project-workbench flow.

## [0.1.0-alpha.1] - 2026-08-12

### Added
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,11 @@ and session-encryption keys, and an explicit fixed internal API URL. Missing or
auth/session infrastructure fails closed and never enables an in-memory fallback.

The `/projects` workbench now demonstrates tenant-scoped project creation, bounded manual or
UTF-8 TXT/CSV/JSON corpus import, immutable version inspection, deterministic exports, and
owner/admin-confirmed project deletion.
UTF-8 TXT/CSV/JSON corpus import, append-only immutable corpus versions with explicit parent
lineage, version inspection, deterministic exports, and owner/admin-confirmed project deletion.
The API router is integrated with the durable control plane through one application-owned
database lifecycle; see [`docs/product/project-workspaces.md`](https://github.com/jemsbhai/corpuskit/blob/main/docs/product/project-workspaces.md).
Project/corpus update, individual corpus deletion, and creation of later corpus versions are not
presented as available.
Project/corpus metadata update and individual corpus deletion are not presented as available.

The application also includes bounded G2P, PHOIBLE inventory, evaluation, distribution/text
quality/error-rate/trajectory analysis, six-algorithm selection comparison, repository
Expand Down
3 changes: 3 additions & 0 deletions apps/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ npm run test:e2e --workspace @corpuskit/web
CORPUSKIT_LIVE_BASE_URL=http://127.0.0.1:3000 npm run test:e2e:live --workspace @corpuskit/web
```

The explicit live runner executes both the end-to-end demo and the project workspace history
flow, including a real immutable successor append and historical-version export.

The scoped workbench configuration enforces at least 90% branch coverage per new state/transport
module. Playwright runs every workbench route through Chromium, Firefox, and WebKit with axe,
console/page/request failure checks, keyboard reachability, and a 320-pixel/200%-text layout
Expand Down
24 changes: 18 additions & 6 deletions apps/web/e2e/projects-live.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { expect, test } from "@playwright/test";

test.skip(
!process.env.CORPUSKIT_LIVE_STACK,
"set CORPUSKIT_LIVE_STACK=1 for real API acceptance",
);

test("real stack creates a tenant project and immutable manual corpus", async ({
test("real stack creates a corpus, appends a version, and preserves history", async ({
page,
}) => {
const suffix = `${Date.now()}-${test.info().project.name}`;
Expand All @@ -24,6 +19,23 @@ test("real stack creates a tenant project and immutable manual corpus", async ({
await expect(
page.getByRole("table", { name: /Normalized sentences/ }),
).toContainText("你好世界");
await page.getByLabel("Version eSpeak language").fill("en-gb");
await page
.getByLabel("Version sentences")
.fill("Revised hello world\nA second immutable sentence");
await page.getByRole("button", { name: "Create version" }).click();
await expect(page.getByRole("button", { name: /Version 2/ })).toHaveAttribute(
"aria-pressed",
"true",
);
await expect(
page.getByRole("table", { name: /Normalized sentences/ }),
).toContainText("A second immutable sentence");

await page.getByRole("button", { name: /Version 1/ }).click();
await expect(
page.getByRole("table", { name: /Normalized sentences/ }),
).toContainText("你好世界");
const downloadPromise = page.waitForEvent("download");
await page.getByRole("link", { name: "JSON" }).click();
const download = await downloadPromise;
Expand Down
84 changes: 84 additions & 0 deletions apps/web/e2e/projects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ const version = {
corpusgen_version: "0.1.7",
created_at: "2026-08-11T00:00:00Z",
};
const versionTwo = {
...version,
id: "00000000-0000-4000-8000-000000000104",
parent_version_id: version.id,
version_number: 2,
language: "en-gb",
content_sha256: "b".repeat(64),
};

test("manual corpus workflow is keyboard-accessible and exports are downloadable", async ({
page,
Expand Down Expand Up @@ -147,3 +155,79 @@ test("CSV import requires an explicit text column", async ({ page }) => {
expect(uploadBody).toContain("seed.csv");
expect(uploadBody).toContain("text/csv");
});

test("manual corpus version creation refreshes and selects immutable history", async ({
page,
}) => {
let submitted: unknown = null;
let appended = false;
await page.route("**/api/v1/**", async (route) => {
const request = route.request();
const path = new URL(request.url()).pathname;
if (path === "/api/v1/auth/me" && request.method() === "GET") {
await route.fulfill({ json: principal });
} else if (path === "/api/v1/projects" && request.method() === "GET") {
await route.fulfill({ json: [project] });
} else if (
path.endsWith(`/projects/${project.id}/corpora`) &&
request.method() === "GET"
) {
await route.fulfill({ json: [corpus] });
} else if (
path.endsWith(`/corpora/${corpus.id}/versions`) &&
request.method() === "POST"
) {
submitted = request.postDataJSON();
appended = true;
await route.fulfill({ status: 201, json: versionTwo });
} else if (
path.endsWith(`/corpora/${corpus.id}/versions`) &&
request.method() === "GET"
) {
await route.fulfill({
json: appended ? [version, versionTwo] : [version],
});
} else if (path.endsWith(`/versions/${version.id}/sentences`)) {
await route.fulfill({
json: [
{ ordinal: 0, original_text: "First", normalized_text: "First" },
],
});
} else if (path.endsWith(`/versions/${versionTwo.id}/sentences`)) {
await route.fulfill({
json: [
{
ordinal: 0,
original_text: " Revised ",
normalized_text: "Revised",
},
],
});
} else {
await route.fulfill({ status: 404 });
}
});

await page.goto("/projects");
await page.getByRole("button", { name: /Demo project/ }).click();
await page.getByRole("button", { name: /Unicode seed/ }).click();
await page.getByLabel("Version eSpeak language").fill("en-gb");
await page.getByLabel("Version sentences").fill(" Revised ");
await page.getByRole("button", { name: "Create version" }).click();

await expect
.poll(() => submitted)
.toEqual({
language: "en-gb",
sentences: [" Revised "],
});
await expect(page.getByRole("button", { name: /Version 2/ })).toHaveAttribute(
"aria-pressed",
"true",
);
await expect(
page.getByRole("table", { name: /Normalized sentences/ }),
).toContainText("Revised");
const accessibility = await new AxeBuilder({ page }).analyze();
expect(accessibility.violations).toEqual([]);
});
2 changes: 1 addition & 1 deletion apps/web/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
testDir: "./e2e",
testIgnore: ["**/demo-live.spec.ts"],
testIgnore: ["**/demo-live.spec.ts", "**/projects-live.spec.ts"],
fullyParallel: true,
forbidOnly: Boolean(process.env.CI),
// Flaky acceptance checks must fail visibly; CI does not conceal them with retries.
Expand Down
2 changes: 1 addition & 1 deletion apps/web/playwright.live.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ if (!baseURL)

export default defineConfig({
testDir: "./e2e",
testMatch: "demo-live.spec.ts",
testMatch: ["demo-live.spec.ts", "projects-live.spec.ts"],
fullyParallel: false,
forbidOnly: true,
retries: 0,
Expand Down
29 changes: 28 additions & 1 deletion apps/web/src/app/projects.css
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,32 @@
margin-top: 2rem;
}

.version-layout {
margin-top: 2rem;
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(18rem, 0.65fr);
align-items: start;
gap: 1rem;
}

.version-layout .version-browser {
min-width: 0;
margin-top: 0;
}

.version-form > .field-help {
margin: -0.5rem 0 1rem;
}

.version-permissions-note {
margin: 0;
padding: 1rem;
border: 1px solid var(--line);
border-radius: var(--radius-md);
background: var(--paper);
color: var(--ink-soft);
}

.version-strip {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
Expand Down Expand Up @@ -333,7 +359,8 @@
@media (max-width: 900px) {
.projects-intro,
.project-layout,
.corpus-layout {
.corpus-layout,
.version-layout {
grid-template-columns: 1fr;
gap: 1.4rem;
}
Expand Down
14 changes: 7 additions & 7 deletions apps/web/src/app/projects/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ProjectWorkbench } from "@/components/project-workbench";
export const metadata: Metadata = {
title: "Project workspaces",
description:
"Create projects and import, inspect, and export immutable speech corpora.",
"Create projects and build, inspect, and export immutable speech corpus histories.",
};

export default function ProjectsPage() {
Expand All @@ -21,14 +21,14 @@ export default function ProjectsPage() {
<div>
<p>
Build a reproducible corpus from manual sentences or a bounded UTF-8
file, then inspect its immutable initial version and verified
exports.
file, add immutable successor versions, then inspect any snapshot
and its verified exports.
</p>
<p className="honesty-note">
This release creates and reads projects and version-1 corpora, and
owners/admins can request retention-governed project deletion with
exact confirmation. Project updates, individual corpus deletion, and
later corpus versions are not yet available.
Owners, admins, and editors can create corpora and later immutable
versions. Owners/admins can request retention-governed project
deletion with exact confirmation. Project updates and individual
corpus deletion are not yet available.
</p>
</div>
</header>
Expand Down
Loading
Loading