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
5 changes: 5 additions & 0 deletions .changeset/fluffy-masks-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/ds-components": patch
---

add arrow-right-arrow-left icon
5 changes: 5 additions & 0 deletions .changeset/polite-snakes-send.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut-core": patch
---

improve and expand instance action schemas
5 changes: 5 additions & 0 deletions .changeset/young-kids-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut": patch
---

add AI assistant
192 changes: 192 additions & 0 deletions .claude/skills/fractal-file-structuring/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
---
name: fractal-file-structuring
description: "Use when creating, moving, splitting, or organizing TypeScript files and folders. Applies fractal tree file-structuring rules which reduce the cognitive overhead of choosing where to put files and ultimately navigating a codebase (once the structure is established and understood)."
license: MIT
metadata:
triggers:
type: domain
enforcement: suggest
priority: high
keywords:
- TypeScript
- JavaScript
- file structure
- folder structure
- create file
- create folder
- split file
- shared folder
intent-patterns:
- "\\b(create|add|move|split|organize|refactor)\\b.*?\\b(file|folder|directory|module|component|hook|type|helper)\\b"
- "\\b(file|folder|directory)\\b.*?\\b(structure|layout|organization|placement)\\b"
---

# Fractal File Structuring

TypeScript and JavaScript files should be organised in a fractal tree structure. Use this skill when deciding where to create, move, split, or organize files and folders in a TypeScript or JavaScript workspace.

This guidance is based on HASH's file-structuring approach: https://hash.dev/blog/file-structuring

## Scope

Apply this skill to TypeScript and JavaScript source files, including modules, components, hooks, helpers, types, tests, scripts, and entry points.

## Core Rules

### Use kebab-case names

Use kebab-case for all TypeScript and JavaScript file and folder names.

```text
create-worker-factory.ts
playback-settings-menu.tsx
button.tsx
```

Avoid PascalCase, camelCase, and mixed-case file names, even for React components.

### Do not create index files

Do not add `index.ts`, `index.tsx`, `index.js`, or `index.jsx` files for folder imports. Prefer explicit file entry points with meaningful names.

If a subtree needs a public entry point, name that file after the concept it exposes (e.g. `schema.ts`)

### Treat each file as a mini-library

A file should expose one or more named exports with a shared semantic purpose. The file name should summarize that purpose (e.g. `users.ts`)

If a file contains only one main export, prefer naming the file after that export in kebab-case (e.g. `create-user.ts`)

Avoid default exports unless a framework or external API requires them.

### Split outgrown files into private subtrees

When a file becomes too large or contains implementation details worth extracting, create a same-named folder next to it and move private pieces there.

```text
editor-view.tsx # public mini-library: the component other files import
editor-view/
panels.tsx # private entry point imported by editor-view.tsx
panels/
simulate-view.tsx # private to panels.tsx
calculate-timeline-range.ts # private helper used only by editor-view.tsx
create-panel-state.ts # private helper used only by editor-view.tsx
```

Only `editor-view.tsx` should import from direct child mini-libraries such as `editor-view/panels.tsx` and `editor-view/calculate-timeline-range.ts`. Only `editor-view/panels.tsx` should import from `editor-view/panels/*.tsx`. Other files should import from `editor-view.tsx`, not from its private subtree. This keeps `editor-view.tsx` as the API boundary and makes `editor-view/` read as its implementation.

If `editor-view/calculate-timeline-range.ts` grows and needs its own private implementation files, create `editor-view/calculate-timeline-range/`. Only `editor-view/calculate-timeline-range.ts` should import from that deeper subtree.

```text
editor-view/
calculate-timeline-range.ts
calculate-timeline-range/
clamp-time.ts # private to calculate-timeline-range.ts
get-visible-duration.ts # private to calculate-timeline-range.ts
```

### Keep private subtrees private

Do not import directly from another file's implementation folder.

```typescript
// Avoid: reaches into another file's private subtree
import { SimulateView } from "../editor-view/panels/simulate-view";

// Prefer (1): import from a public mini-library (if it is conceptually part of editor-view)
import { EditorView } from "../editor-view";

// Prefer (2): move shared code to a shared folder (if it is NOT conceptually part of editor-view)
import { Button } from "../shared/button";
```

If a resource must be available outside the subtree, re-export it from the subtree root only when it is part of that root's public concept. If it is independently useful to sibling branches, move it to an appropriate `shared/` folder instead.

### Put shared resources at the closest fork

When multiple sibling branches need the same helper, type, component, constant, or hook, place it in the nearest applicable `shared/` folder.

```text
editor-view.tsx
editor-view/
shared/
duration-label.tsx # used by both panels.tsx and bottom-section.tsx
playback-time.ts # shared formatting/parsing logic for this subtree
panels.tsx # imports from panels/
panels/
simulate-view.tsx # private to panels.tsx
bottom-section.tsx # imports from bottom-section/
bottom-section/
bottom-bar.tsx # private to bottom-section.tsx
```

Place shared files as deep as possible while still covering all current consumers. Do not move something to a high-level shared folder just because it might be reused later.

Here `editor-view.tsx` imports `./editor-view/panels` and `./editor-view/bottom-section`. `panels.tsx` may import `./panels/simulate-view` and `./shared/duration-label`; `bottom-section.tsx` may import `./bottom-section/bottom-bar` and `./shared/duration-label`. Nothing else should import from `panels/` or `bottom-section/` directly.

Shared files are mini-libraries too. A shared file can have its own private same-named subtree, and those internals should remain private to that shared file.

```text
editor-view/
shared/
playback-time.ts # public to editor-view/* branches
playback-time/
parse-playback-time.ts # private to playback-time.ts
format-playback-time.ts # private to playback-time.ts
```

If later only `bottom-bar.tsx` uses `duration-label.tsx`, move it beside `bottom-bar.tsx` or under `bottom-bar/`. The folder structure should describe current consumers, not preserve old sharing.

### Use relative imports within a workspace

For imports inside the same workspace, use relative paths. Do not introduce workspace-local aliases just to shorten paths.

Imports from other workspaces should use the package name.

### Co-locate unit tests

Place unit tests next to the file they cover.

```text
foo.ts
foo.test.ts
```

If a private extracted file needs direct tests, place those tests next to that extracted file.

```text
editor-view.tsx
editor-view.test.tsx
editor-view/
calculate-timeline-range.ts
calculate-timeline-range.test.ts
```

Prefer testing through the public mini-library when that gives enough coverage. Add direct tests for private extracted files when the logic is complex enough that tests through the owner would be indirect or brittle.

### Match the current shape

Organize files for the code's current relationships, not speculative future reuse. Moving files later is expected and cheaper than adding premature structure now.

## Decision Checklist

Before creating a TypeScript or JavaScript file or folder:

1. Identify the semantic concept the file represents.
2. Name the file or folder in kebab-case.
3. If extracting from an existing file, put private implementation files under a same-named folder.
4. If multiple current branches need the resource, put it in the nearest `shared/` folder.
5. Avoid `index` files and implicit folder imports.
6. Use relative imports within the workspace.
7. Co-locate tests with the file under test.

## When Unsure

Choose the location that communicates the file's current consumers and API boundary most clearly:

- Private implementation detail: place it under the owning file's same-named folder, and import it only from that owner.
- Named mini-library: create a normal named file when the concept has its own purpose and exports a small API for nearby consumers.
- Shared mini-library: place the named file in the closest `shared/` folder when multiple branches need that API.
- Subtree entry point: expose the public API from a named root file, and keep any deeper implementation files private to that root.

Do not add broad `components`, `hooks`, `utils`, `types`, or `services` folders unless absolutely necessary. If they exist, these folders MUST only be imported from by files called `components.ts`, `hooks.ts`, etc.
30 changes: 30 additions & 0 deletions .claude/skills/skill-rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,36 @@
"blockMessage": "Skill is required to proceed",
"skipConditions": {}
},
"fractal-file-structuring": {
"type": "domain",
"enforcement": "suggest",
"priority": "high",
"description": "Use when creating, moving, splitting, or organizing TypeScript files and folders. Applies fractal tree file-structuring rules which reduce the cognitive overhead of choosing where to put files and ultimately navigating a codebase (once the structure is established and understood).",
"promptTriggers": {
"keywords": [
"TypeScript",
"JavaScript",
"file structure",
"folder structure",
"create file",
"create folder",
"split file",
"shared folder"
],
"intentPatterns": [
"\\b(create|add|move|split|organize|refactor)\\b.*?\\b(file|folder|directory|module|component|hook|type|helper)\\b",
"\\b(file|folder|directory)\\b.*?\\b(structure|layout|organization|placement)\\b"
]
},
"fileTriggers": {
"include": [],
"exclude": [],
"content": [],
"create-only": false
},
"blockMessage": "Skill is required to proceed",
"skipConditions": {}
},
"handling-rust-errors": {
"type": "domain",
"enforcement": "suggest",
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,4 @@ out/

# Jujutsu
.jj/
.vercel
2 changes: 1 addition & 1 deletion apps/mcp/linear/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@local/tsconfig": "workspace:*",
"@modelcontextprotocol/sdk": "1.26.0",
"dotenv-flow": "3.3.0",
"zod": "4.1.12",
"zod": "4.4.3",
"zod-to-json-schema": "3.24.6"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion apps/mcp/notion/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@notionhq/client": "5.3.0",
"dotenv-flow": "3.3.0",
"notion-to-md": "3.1.9",
"zod": "4.1.12",
"zod": "4.4.3",
"zod-to-json-schema": "3.24.6"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions apps/petrinaut-website/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OPENAI_API_KEY=sk-xxxx
89 changes: 89 additions & 0 deletions apps/petrinaut-website/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Petrinaut Website

A website for demoing Petrinaut (libs/@hashintel/petrinaut).

A SPA plus a single API function that proxies AI requests to OpenAI.

## Quickstart

```sh
cp .env.example .env.local
# add your OPENAI_API_KEY to .env.local, if you want to use the chat feature

turbo run dev
```

The dev server runs at [http://localhost:5173](http://localhost:5173). A plugin in `vite.config.ts` loads the API function.

In production, the function in the `api` folder is automatically deployed as a Vercel Serverless Function.

## Environment variables

| Name | Required | Used by | Notes |
| -------------------- | ---------------- | ---------------- | --------------------------------------------------------- |
| `OPENAI_API_KEY` | for chat to work | `api/chat.ts` | OpenAI key the function uses to call `streamText`. |
| `PETRINAUT_AI_MODEL` | no | `api/chat.ts` | Overrides the default OpenAI model id. |
| `SENTRY_DSN` | no | `vite.config.ts` | Wired into the bundle via `__SENTRY_DSN__` at build time. |

Local values live in `.env.local`; Vite's `loadEnv` (see [`vite.config.ts`](vite.config.ts)) copies them into `process.env` for both the dev server and the chat function. In production, set these in the Vercel project settings.

## Testing the API against the built output

A plain `yarn build && yarn vite preview` only serves the static `dist/` assets - `/api/chat` will 404 because the dev plugin is not loaded by `vite preview`. Use one of the options below to exercise the production code path locally.

### Option A: `vercel dev` (recommended)

Closest to the real Vercel runtime. It builds the site, bundles the function, and serves both from a single port using the actual Node runtime + routing layer.

Requires linking to a Vercel project. If you don't have access, go for Option B (or just use `turbo run dev` instead).

```sh
cd apps/petrinaut-website

npx vercel link # first-time setup

npx vercel dev # builds + serves on http://localhost:3000
```

Notes:

- `vercel dev` does not read your existing `dist/`; it rebuilds. If you specifically need to inspect the artifact you already produced, use option B (or amend the devCommand in vercel.json to remove the build step).

### Option B: `vite preview` + a sibling Node API server

Useful when you want to serve the literal `dist/` artifact you just built and avoid the Vercel CLI. It is two processes, glued together by `preview.proxy`.

1. Add a proxy entry to `vite.config.ts` (only needed while you are testing this flow):

```ts
preview: {
proxy: { "/api": "http://localhost:3001" },
},
```

2. Create a throwaway `scripts/preview-api.mjs` that mounts the same handler with `createServerAdapter`:

```js
import { createServer } from "node:http";
import { createServerAdapter } from "@whatwg-node/server";
import handler from "../api/chat.ts";

createServer(createServerAdapter(handler)).listen(3001, () => {
console.log("preview API listening on http://localhost:3001");
});
```

3. Run them side by side (Node 22.6+ can execute the TypeScript entry directly with `--experimental-strip-types`):

```sh
yarn build
yarn vite preview # :4173
node --experimental-strip-types scripts/preview-api.mjs # :3001
```

`/api/chat` requests against `:4173` will be proxied to the local API server, which loads the same handler the deployed function uses.

## Known caveats

- **In-memory rate limiting.** [`api/chat.ts`](api/chat.ts) keys rate-limit buckets by the client IP that Vercel's edge writes into `x-forwarded-for` (which Vercel actively prevents the caller from spoofing - see the [request headers docs](https://vercel.com/docs/edge-network/headers/request-headers)). The bucket map lives in module scope, so it resets on cold start and is not shared between concurrent function instances.
- **`vercel-build.sh` deletes the repo-root `.env`.** This is intentional (mise picks it up otherwise), but worth knowing if you run `vercel dev` locally and keep secrets there.
Loading
Loading