Skip to content
Merged
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
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,20 @@ and mitigation vertical slice:
or excluded replacements without changing the organization baseline
- Selectable graph nodes, original/mitigation toggling, and an accessible text
representation of every relationship path
- Flyway-managed relational schema and Harborline Commerce seed
- Flyway-managed relational schema with published Northstar demo data
- Backend unit and real-PostgreSQL integration tests
- Frontend unit tests

## Seeded Harborline baseline
## Published Northstar demo

The version 1 baseline contains 25 employees, five teams, eight roles, six
applications, 23 permissions, ten capabilities, four workflows, and eleven
workflow steps. It includes the primary Priya Sharma role-revocation scenario,
Bob Chen as a potential mitigation candidate, and supporting refund and release
coverage scenarios.
The public version 1 demo models Northstar Medical Supplies with six employees,
two teams, four roles, and the critical Vendor Payment Run. Daniel Brooks is the
only Bank Payment Releaser, so removing that assignment blocks the bank-release
step. Nia Kapoor is ranked first as a safe replacement because she is active,
works in the same Treasury Operations team, already has Bank Portal access, and
restores the workflow without worsening another process. The published snapshot
is read-only. The older Harborline seed remains available for regression coverage
but is no longer presented as the product demo.

## Prerequisites

Expand Down Expand Up @@ -74,10 +77,9 @@ npm --prefix frontend install
npm --prefix frontend run dev
```

Open <http://localhost:5173>. Run the Priya Sharma scenario, inspect the red and
amber impact paths, review Bob Chen's recommendation, and test the mitigation to
see the restored green relationship graph alongside the Low residual access
impact.
Open <http://localhost:5173>. Choose **Explore the live demo**, inspect the
Northstar dependency graph, test Daniel Brooks's access removal, and verify Nia
Kapoor's mitigation to see the restored green relationship graph.

Editable organizations can be reopened from the homepage using their public ID
with or without the displayed hyphen. Anyone with an organization ID or link can
Expand All @@ -86,7 +88,7 @@ personal data.

## Frontend routes

- `/example` opens the read-only Harborline example.
- `/example` opens the read-only, graph-first Northstar continuity demo.
- `/organizations/{publicCode}/map` opens an editable organization map.
- `/organizations/{publicCode}/inventory` opens its detailed inventory.
- `/organizations/{publicCode}/impact` opens a fresh impact-testing screen.
Expand All @@ -100,7 +102,7 @@ Production hosting must rewrite non-asset, non-API routes to

## API endpoints

- `GET /api/v1/dashboard` loads the default Harborline Commerce dashboard.
- `GET /api/v1/dashboard` loads the default Northstar Medical Supplies dashboard.
- `GET /api/v1/dashboard?organization={slug}` loads another organization or
returns `404` when the slug does not exist.
- `POST /api/v1/simulations` runs and saves a revoke-role simulation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public DashboardController(DashboardQueryService dashboardQueryService) {

@GetMapping
public DashboardResponse getDashboard(
@RequestParam(name = "organization", defaultValue = "harborline-commerce") String organizationSlug) {
@RequestParam(name = "organization", defaultValue = "northstar-public-demo") String organizationSlug) {
return dashboardQueryService.getDashboard(organizationSlug);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,34 @@ public WorkspaceResource getEditableByCode(String rawCode) {
@Transactional
public WorkspaceResource createBlank(WorkspaceRequest request) {
String name = request.name().trim();
String slug = resolveSlug(request, name);
if (workspaceRepository.existsBySlug(slug)) {
throw slugConflict(slug);
String baseSlug = resolveSlug(request, name);
boolean generatedSlug = request.slug() == null;
if (!generatedSlug && workspaceRepository.existsBySlug(baseSlug)) {
throw slugConflict(baseSlug);
}

String slug = baseSlug;
for (int attempt = 0; attempt < 5; attempt++) {
UUID workspaceId = UUID.randomUUID();
String publicCode = workspaceCode.generate(name);
if (workspaceRepository.insertDraft(workspaceId, slug, name, publicCode)) {
return get(workspaceId);
}
if (workspaceRepository.existsBySlug(slug)) {
throw slugConflict(slug);
if (!generatedSlug) {
throw slugConflict(slug);
}
slug = collisionSafeSlug(baseSlug, workspaceId);
}
}
throw new WorkspaceConflictException("A unique organization ID could not be generated; try again");
}

private String collisionSafeSlug(String baseSlug, UUID workspaceId) {
String suffix = "-" + workspaceId.toString().substring(0, 8);
return baseSlug.substring(0, Math.min(baseSlug.length(), 80 - suffix.length())) + suffix;
}

private WorkspaceConflictException slugConflict(String slug) {
return new WorkspaceConflictException("A workspace with slug '" + slug + "' already exists");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
INSERT INTO organizations (
id, slug, name, current_version, created_at, workspace_status, updated_at, public_code
) VALUES (
'00000000-0000-0000-0000-000000000002',
'northstar-public-demo',
'Northstar Medical Supplies',
1,
'2026-09-16T12:00:00Z',
'PUBLISHED',
'2026-09-16T12:00:00Z',
'NMS-NQ7K4P9D8XM2QR6W'
);

INSERT INTO teams (id, organization_id, name, department) VALUES
('11000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000002', 'Accounts Payable', 'Finance'),
('11000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000002', 'Treasury Operations', 'Treasury');

INSERT INTO employees (id, organization_id, team_id, employee_no, name, email, status, region, shift) VALUES
('22000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000002', '11000000-0000-0000-0000-000000000001', 'NMS-01', 'Maya Singh', 'maya.singh@northstar.demo', 'ACTIVE', 'NORTH_AMERICA', 'DAY'),
('22000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000002', '11000000-0000-0000-0000-000000000001', 'NMS-02', 'Arjun Mehta', 'arjun.mehta@northstar.demo', 'ACTIVE', 'NORTH_AMERICA', 'DAY'),
('22000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000002', '11000000-0000-0000-0000-000000000001', 'NMS-03', 'Priya Sharma', 'priya.sharma@northstar.demo', 'ACTIVE', 'NORTH_AMERICA', 'DAY'),
('22000000-0000-0000-0000-000000000004', '00000000-0000-0000-0000-000000000002', '11000000-0000-0000-0000-000000000001', 'NMS-04', 'Sarah Chen', 'sarah.chen@northstar.demo', 'ACTIVE', 'NORTH_AMERICA', 'DAY'),
('22000000-0000-0000-0000-000000000005', '00000000-0000-0000-0000-000000000002', '11000000-0000-0000-0000-000000000002', 'NMS-05', 'Daniel Brooks', 'daniel.brooks@northstar.demo', 'ACTIVE', 'NORTH_AMERICA', 'DAY'),
('22000000-0000-0000-0000-000000000006', '00000000-0000-0000-0000-000000000002', '11000000-0000-0000-0000-000000000002', 'NMS-06', 'Nia Kapoor', 'nia.kapoor@northstar.demo', 'ACTIVE', 'NORTH_AMERICA', 'DAY');

UPDATE teams SET manager_employee_id = '22000000-0000-0000-0000-000000000003'
WHERE id = '11000000-0000-0000-0000-000000000001';
UPDATE teams SET manager_employee_id = '22000000-0000-0000-0000-000000000005'
WHERE id = '11000000-0000-0000-0000-000000000002';

INSERT INTO roles (id, organization_id, name, description, sensitivity, owner_employee_id) VALUES
('33000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000002', 'Invoice Processor', 'Validates supplier invoices before payment approval.', 'MEDIUM', '22000000-0000-0000-0000-000000000003'),
('33000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000002', 'Payment Approver', 'Approves validated supplier payments.', 'HIGH', '22000000-0000-0000-0000-000000000003'),
('33000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000002', 'Bank Payment Releaser', 'Releases approved payment batches to the bank.', 'CRITICAL', '22000000-0000-0000-0000-000000000005'),
('33000000-0000-0000-0000-000000000004', '00000000-0000-0000-0000-000000000002', 'Treasury Viewer', 'Reviews the bank release queue without release authority.', 'LOW', '22000000-0000-0000-0000-000000000005');

INSERT INTO employee_roles (employee_id, role_id, assigned_at, assigned_by) VALUES
('22000000-0000-0000-0000-000000000001', '33000000-0000-0000-0000-000000000001', '2026-09-16T12:00:00Z', 'seed:northstar-demo'),
('22000000-0000-0000-0000-000000000002', '33000000-0000-0000-0000-000000000001', '2026-09-16T12:00:00Z', 'seed:northstar-demo'),
('22000000-0000-0000-0000-000000000003', '33000000-0000-0000-0000-000000000002', '2026-09-16T12:00:00Z', 'seed:northstar-demo'),
('22000000-0000-0000-0000-000000000004', '33000000-0000-0000-0000-000000000002', '2026-09-16T12:00:00Z', 'seed:northstar-demo'),
('22000000-0000-0000-0000-000000000005', '33000000-0000-0000-0000-000000000003', '2026-09-16T12:00:00Z', 'seed:northstar-demo'),
('22000000-0000-0000-0000-000000000006', '33000000-0000-0000-0000-000000000004', '2026-09-16T12:00:00Z', 'seed:northstar-demo');

INSERT INTO applications (id, organization_id, name, category, owner_employee_id) VALUES
('44000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000002', 'Accounts Payable Hub', 'Finance', '22000000-0000-0000-0000-000000000003'),
('44000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000002', 'Bank Portal', 'Treasury', '22000000-0000-0000-0000-000000000005');

INSERT INTO resources (id, application_id, name, resource_type) VALUES
('55000000-0000-0000-0000-000000000001', '44000000-0000-0000-0000-000000000001', 'Supplier Invoices', 'invoice_queue'),
('55000000-0000-0000-0000-000000000002', '44000000-0000-0000-0000-000000000001', 'Payment Batches', 'payment_batch'),
('55000000-0000-0000-0000-000000000003', '44000000-0000-0000-0000-000000000002', 'Bank Release Queue', 'bank_queue');

INSERT INTO permissions (id, organization_id, application_id, resource_id, action, sensitivity) VALUES
('66000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000002', '44000000-0000-0000-0000-000000000001', '55000000-0000-0000-0000-000000000001', 'invoice.validate', 'MEDIUM'),
('66000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000002', '44000000-0000-0000-0000-000000000001', '55000000-0000-0000-0000-000000000002', 'payment.approve', 'HIGH'),
('66000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000002', '44000000-0000-0000-0000-000000000002', '55000000-0000-0000-0000-000000000003', 'payment.release', 'CRITICAL'),
('66000000-0000-0000-0000-000000000004', '00000000-0000-0000-0000-000000000002', '44000000-0000-0000-0000-000000000002', '55000000-0000-0000-0000-000000000003', 'payment.read', 'LOW');

INSERT INTO role_permissions (role_id, permission_id) VALUES
('33000000-0000-0000-0000-000000000001', '66000000-0000-0000-0000-000000000001'),
('33000000-0000-0000-0000-000000000002', '66000000-0000-0000-0000-000000000002'),
('33000000-0000-0000-0000-000000000003', '66000000-0000-0000-0000-000000000003'),
('33000000-0000-0000-0000-000000000004', '66000000-0000-0000-0000-000000000004');

INSERT INTO capabilities (id, organization_id, name, description) VALUES
('77000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000002', 'Validate supplier invoice', 'Validate invoice details before payment approval.'),
('77000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000002', 'Approve supplier payment', 'Approve a validated supplier payment batch.'),
('77000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000002', 'Release bank payment', 'Release the approved batch to the bank.');

INSERT INTO capability_permissions (capability_id, permission_id) VALUES
('77000000-0000-0000-0000-000000000001', '66000000-0000-0000-0000-000000000001'),
('77000000-0000-0000-0000-000000000002', '66000000-0000-0000-0000-000000000002'),
('77000000-0000-0000-0000-000000000003', '66000000-0000-0000-0000-000000000003');

INSERT INTO workflows (id, organization_id, name, criticality, owner_employee_id) VALUES
('88000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000002', 'Vendor Payment Run', 'CRITICAL', '22000000-0000-0000-0000-000000000003');

INSERT INTO workflow_steps (
id, workflow_id, step_key, name, position, required_capability_id,
minimum_actors, resilience_target, required_department, required_region,
required_shift, required_application_id
) VALUES
('99000000-0000-0000-0000-000000000001', '88000000-0000-0000-0000-000000000001', 'VALIDATE_INVOICE', 'Validate supplier invoice', 1, '77000000-0000-0000-0000-000000000001', 1, 2, 'Finance', NULL, NULL, '44000000-0000-0000-0000-000000000001'),
('99000000-0000-0000-0000-000000000002', '88000000-0000-0000-0000-000000000001', 'APPROVE_PAYMENT', 'Approve supplier payment', 2, '77000000-0000-0000-0000-000000000002', 1, 2, 'Finance', NULL, NULL, '44000000-0000-0000-0000-000000000001'),
('99000000-0000-0000-0000-000000000003', '88000000-0000-0000-0000-000000000001', 'RELEASE_PAYMENT', 'Release bank payment', 3, '77000000-0000-0000-0000-000000000003', 1, 1, 'Treasury', NULL, NULL, '44000000-0000-0000-0000-000000000002');

INSERT INTO workflow_constraints (id, workflow_id, type, parameters) VALUES
('aa000000-0000-0000-0000-000000000001', '88000000-0000-0000-0000-000000000001', 'DIFFERENT_ACTORS', '{"description":"Invoice validation, payment approval, and bank release must be performed by different people."}');

INSERT INTO workflow_step_constraints (workflow_step_id, constraint_id) VALUES
('99000000-0000-0000-0000-000000000001', 'aa000000-0000-0000-0000-000000000001'),
('99000000-0000-0000-0000-000000000002', 'aa000000-0000-0000-0000-000000000001'),
('99000000-0000-0000-0000-000000000003', 'aa000000-0000-0000-0000-000000000001');

INSERT INTO organization_versions (organization_id, version, content_hash, created_at) VALUES
('00000000-0000-0000-0000-000000000002', 1, '9ae6989ed3265701f96e57c20d5704223d830abdfeeb93593077c4a694716cb5', '2026-09-16T12:00:00Z');
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ class CatalogMigrationIntegrationTest {
private static final UUID PRIYA_ID = UUID.fromString("20000000-0000-0000-0000-000000000001");
private static final UUID BOB_ID = UUID.fromString("20000000-0000-0000-0000-000000000002");
private static final UUID FINANCE_APPROVER_ID = UUID.fromString("30000000-0000-0000-0000-000000000002");
private static final UUID NORTHSTAR_ID = UUID.fromString("00000000-0000-0000-0000-000000000002");
private static final UUID DANIEL_ID = UUID.fromString("22000000-0000-0000-0000-000000000005");
private static final UUID NIA_ID = UUID.fromString("22000000-0000-0000-0000-000000000006");
private static final UUID BANK_PAYMENT_RELEASER_ID = UUID.fromString("33000000-0000-0000-0000-000000000003");

@Container
@ServiceConnection
Expand Down Expand Up @@ -88,24 +92,25 @@ SELECT COUNT(*)
""").query(Integer.class).single();

assertThat(publicCodes).isNotEmpty()
.allSatisfy(code -> assertThat(code).matches("[A-HJ-NP-Z2-9]{3}-[A-F0-9]{32}"));
.allSatisfy(code -> assertThat(code).matches(
"[A-HJ-NP-Z2-9]{3}-(?:[A-HJ-NP-Z2-9]{16}|[A-F0-9]{32})"));
assertThat(publicCodes.stream().map(String::toUpperCase).distinct().count())
.isEqualTo(publicCodes.size());
assertThat(cloneColumnCount).isZero();
}

@Test
void appliesSchemaAndLoadsTheCompleteHarborlineBaseline() {
assertThat(count("organizations")).isEqualTo(1);
assertThat(count("teams")).isEqualTo(5);
assertThat(count("employees")).isEqualTo(25);
assertThat(count("roles")).isEqualTo(8);
assertThat(count("applications")).isEqualTo(6);
assertThat(count("permissions")).isEqualTo(23);
assertThat(count("capabilities")).isEqualTo(10);
assertThat(count("workflows")).isEqualTo(4);
assertThat(count("workflow_steps")).isEqualTo(11);
assertThat(count("workflow_constraints")).isEqualTo(3);
void appliesSchemaAndLoadsBothPublishedBaselines() {
assertThat(count("organizations")).isEqualTo(2);
assertThat(count("teams")).isEqualTo(7);
assertThat(count("employees")).isEqualTo(31);
assertThat(count("roles")).isEqualTo(12);
assertThat(count("applications")).isEqualTo(8);
assertThat(count("permissions")).isEqualTo(27);
assertThat(count("capabilities")).isEqualTo(13);
assertThat(count("workflows")).isEqualTo(5);
assertThat(count("workflow_steps")).isEqualTo(14);
assertThat(count("workflow_constraints")).isEqualTo(4);

var successfulMigrations = jdbcClient.sql("""
SELECT COUNT(*)
Expand All @@ -115,7 +120,7 @@ SELECT COUNT(*)
.query(Integer.class)
.single();

assertThat(successfulMigrations).isEqualTo(10);
assertThat(successfulMigrations).isEqualTo(11);
var employeeNumberNullable = jdbcClient.sql("""
SELECT is_nullable
FROM information_schema.columns
Expand Down Expand Up @@ -270,21 +275,51 @@ void assemblesAnImmutableVersionedOrganizationSnapshot() {
}

@Test
void servesTheSeededDashboardFromTheSnapshotBoundary() throws Exception {
void servesNorthstarAsTheDefaultPublishedDemo() throws Exception {
mockMvc.perform(get("/api/v1/dashboard"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.organization.slug").value("harborline-commerce"))
.andExpect(jsonPath("$.organization.slug").value("northstar-public-demo"))
.andExpect(jsonPath("$.organization.name").value("Northstar Medical Supplies"))
.andExpect(jsonPath("$.organization.baselineVersion").value(1))
.andExpect(jsonPath("$.counts.employees").value(25))
.andExpect(jsonPath("$.counts.activeEmployees").value(24))
.andExpect(jsonPath("$.counts.roles").value(8))
.andExpect(jsonPath("$.counts.applications").value(6))
.andExpect(jsonPath("$.counts.permissions").value(23))
.andExpect(jsonPath("$.counts.workflows").value(4))
.andExpect(jsonPath("$.workflows[0].name").value("Production Deployment"))
.andExpect(jsonPath("$.counts.employees").value(6))
.andExpect(jsonPath("$.counts.activeEmployees").value(6))
.andExpect(jsonPath("$.counts.roles").value(4))
.andExpect(jsonPath("$.counts.workflows").value(1))
.andExpect(jsonPath("$.workflows[0].name").value("Vendor Payment Run"))
.andExpect(jsonPath("$.workflows[0].criticality").value("CRITICAL"));
}

@Test
void blocksTheCanonicalNorthstarPaymentRunAndRanksNiaFirst() throws Exception {
mockMvc.perform(post("/api/v1/simulations")
.header("Idempotency-Key", "test-northstar-public-demo")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"schemaVersion":"1.0",
"organizationId":"%s",
"baselineVersion":1,
"change":{"type":"REVOKE_EMPLOYEE_ROLE","employeeId":"%s","roleId":"%s"}
}
""".formatted(NORTHSTAR_ID, DANIEL_ID, BANK_PAYMENT_RELEASER_ID)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.result.overallSeverity").value("CRITICAL"))
.andExpect(jsonPath("$.result.executiveSummary.workflowsBlocked").value(1))
.andExpect(jsonPath("$.result.workflowImpacts[0].workflowName").value("Vendor Payment Run"))
.andExpect(jsonPath("$.result.workflowImpacts[0].scenarioStatus").value("BLOCKED"))
.andExpect(jsonPath("$.result.recommendations[0].candidate.id").value(NIA_ID.toString()))
.andExpect(jsonPath("$.result.recommendations[0].candidate.name").value("Nia Kapoor"));
}

@Test
void rejectsCatalogChangesToThePublishedNorthstarDemo() throws Exception {
mockMvc.perform(post("/api/v1/workspaces/{workspaceId}/catalog/teams", NORTHSTAR_ID)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"Injected team\",\"department\":\"Demo\"}"))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.code").value("DRAFT_CATALOG_CONFLICT"));
}

@Test
void returnsNotFoundForAnUnknownDashboardOrganization() throws Exception {
mockMvc.perform(get("/api/v1/dashboard").param("organization", "missing-company"))
Expand Down
Loading
Loading