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
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: CI

# CI is intentionally not wired to push/PR for now — the maintainer decided
# all verification runs locally during implementation (pnpm lint && pnpm
# typecheck && pnpm test). Restore the push/pull_request triggers when CI is
# re-enabled (story 4.2 revisits the CI recipe).
on:
workflow_dispatch:

jobs:
checks:
name: Lint, typecheck and test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
# Version comes from the packageManager field in package.json.

- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Contract types are freshly generated
# AD-9: the JSON Schema is the source and the TS types are generated
# from it. Regenerating must be a no-op on a clean checkout — a diff
# here means the committed types drifted from the schema.
run: |
pnpm --filter @gitnebula/contract generate
git diff --exit-code -- packages/contract/src/generated

- name: Lint
run: pnpm lint

- name: Typecheck
run: pnpm typecheck

- name: Test
run: pnpm test
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ dist/
build/
*.tsbuildinfo

# Generated fixture repositories (AD-14) — built by scripts, never committed
test-fixtures/.generated/

# BMAD personal settings (per-developer, not shared)
_bmad/config.user.toml
_bmad/custom/config.user.toml

# OS / editor
.DS_Store
*.log

# perf-spike run output (regenerated on every run; the recorded evidence
# lives in docs/dev/epic-1/1.4-perf-spike/results.json)
packages/viz/perf-spike/results.json
17 changes: 17 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Lockfile and build output
pnpm-lock.yaml
dist/
node_modules/
test-fixtures/.generated/

# Frozen / third-party artifacts — append-only, never reformatted
docs/
_bmad/
reference/
CLAUDE.md
.claude/

# terminal-agents worktree scratch files (never committed)
plan.md
DECISIONS.md
PR_SUMMARY.md
1 change: 1 addition & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
75 changes: 75 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Contributing to gitnebula

Thanks for contributing! This document covers the workflow every change —
human- or agent-authored — must follow. The scope source of truth is
[docs/GITNEBULA_PROJECT_BRIEF.md](docs/GITNEBULA_PROJECT_BRIEF.md); process
details live in [CLAUDE.md](CLAUDE.md).

## Getting started

Requirements: Node.js ≥ 20.19 (Vite's floor within the Node 20 line) and
pnpm 10 (the exact version is pinned in the
`packageManager` field — with [corepack](https://nodejs.org/api/corepack.html)
enabled, `pnpm` resolves to it automatically).

```bash
pnpm install # once per clone
pnpm lint # ESLint 9 (flat config) + Prettier check
pnpm typecheck # tsc --noEmit in every package
pnpm test # vitest, one suite per package
pnpm build # tsup (cli) + vite (viz) — the only two build edges
```

`pnpm lint && pnpm test` must both exit 0 before every commit. Run them from
the repository root so the workspace resolves.

## Commit messages: Conventional Commits, scope = module

Commits follow [Conventional Commits](https://www.conventionalcommits.org/)
and the **scope is the module** from the workspace
(`contract | scanner | deps | githist | viz | cli`, or `repo` for root-level
changes) — never the story id:

```
feat(deps): parse TypeScript imports

Refs: 2.2-deps-ts-imports
Agent: alice (terminal-agents, claude-opus-5)
```

### Trailers

- `Refs:` — the story id (`<epic>.<n>-<slug>`), identical to the spec
filename, the branch slug and the sprint-status key. Required on every
story commit.
- `Agent:` — required on commits authored by a terminal-agents agent:
`Agent: <name> (terminal-agents, <model-id>)`. Maintainer commits carry no
`Agent:` trailer. Do **not** use `Co-authored-by:` for agent names — GitHub
tries to resolve it to a real account and produces junk attributions.

## Pull requests: squash merge

PRs are **squash-merged** — one clean commit per story lands on the base
branch:

- The **PR title becomes the squash commit subject**, so it must itself be a
valid Conventional Commit (`type(module): subject`).
- **Both trailers (`Refs:`, `Agent:`) must appear in the PR body** — the
squash body, not the branch commits, is what survives on the base branch.
- The PR description must be understandable without reading the code, and CI
must be green.

## Determinism

`scanner`, `deps` and `githist` are banned from `Date.now()` and
`Math.random()` by an ESLint rule (AD-4): the same repository at the same
commit must produce a byte-identical `analysis.json`. Timestamps in analysis
output come from git, not from the system clock.

## Package boundaries

Allowed dependency edges (AD-2) are exactly: `cli` → all four Node packages +
`contract`; `scanner`/`deps`/`githist`/`viz` → `contract` only; `contract` →
nothing (and no `node:` imports — it is environment-neutral). Deep imports
(`@gitnebula/contract/src/x`) deliberately do not resolve; if you need
something, export it from the package's `exports` map on purpose.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Łukasz Jundziłł

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
66 changes: 66 additions & 0 deletions docs/dev/epic-1/1.1-workspace-scaffold/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 1.1-workspace-scaffold — what the scaffold provides

This story created the pnpm workspace every later story builds on. On a fresh
clone (Node ≥ 20.19 — Vite 8's floor within the Node 20 line — and pnpm 10 via
corepack):

```bash
pnpm install
pnpm lint # ESLint 9 flat config + Prettier check
pnpm typecheck # tsc --noEmit in every package
pnpm test # vitest — one trivial suite per package
pnpm build # tsup (cli) + vite (viz) — the only two build edges (AD-11)
```

## What exists now

- **Six packages** under `packages/`: `@gitnebula/{contract,scanner,deps,githist,viz,cli}`.
All pure ESM (`"type": "module"`), NodeNext resolution, source-only internals
(`exports` maps point at `./src/index.ts`; no build step for
contract/scanner/deps/githist).
- **Narrow `exports` maps** — deep imports like `@gitnebula/contract/src/x`
fail with `ERR_PACKAGE_PATH_NOT_EXPORTED`. If you need a symbol, export it
from the package's `src/index.ts` deliberately.
- **AD-2 dependency edges, physically declared**: `cli` depends on
contract + scanner + deps + githist; the four spokes depend on `contract`
only; `contract` depends on nothing and must stay free of `node:` imports.
- **AD-4 determinism lint**: `Date.now()` / `Math.random()` are lint errors
inside `packages/{scanner,deps,githist}` (rule: `no-restricted-properties`
in `eslint.config.js`). viz and cli are exempt.
- **tsconfig layout**: shared `tsconfig.base.json` (strict, NodeNext, noEmit);
`viz` compiles against DOM lib with no `node:` types; `contract` has neither
DOM nor node types (environment-neutral); scanner/deps/githist/cli are
Node-side.
- **CI**: `.github/workflows/ci.yml` defines install + lint + typecheck + test
(Node 20, pnpm from the `packageManager` field). By maintainer decision it is
currently trigger-disabled (`workflow_dispatch` only) — all verification runs
locally during implementation; story 4.2 revisits CI.
- **Repo hygiene**: MIT `LICENSE`, `CONTRIBUTING.md` (commit convention,
trailers, squash policy), `.gitignore` covers `dist/` and
`test-fixtures/.generated/`.

## How to add a dependency correctly

1. **Workspace package → workspace package**: only along an AD-2 edge, as
`"@gitnebula/<name>": "workspace:*"` in `dependencies`. Any other edge
(viz → analyzer, analyzer → analyzer, anything → cli) is an architecture
violation — stop and escalate rather than declaring it.
2. **External runtime dependency**: add it to the one package that needs it
(`pnpm --filter @gitnebula/<pkg> add <dep>`), pinning the minor at the
package's first story per the architecture stack table.
3. **Shared dev tooling** (used by several packages): add the version to the
`catalog:` section of `pnpm-workspace.yaml` once, then reference it as
`"<dep>": "catalog:"` in each package — versions never drift.
4. **A dependency with an install script**: pnpm 10 blocks install scripts by
default; if the dependency genuinely needs one (like esbuild), add it to
`onlyBuiltDependencies` in `pnpm-workspace.yaml` and say so in your PR.

## Placeholders that later stories replace

- `packages/*/src/index.ts` stubs (each exports `packageName`; spokes
re-export the contract name to smoke-test resolution) — replaced by real
implementations from story 1.2 onward.
- `packages/cli/dist/gitnebula.js` — placeholder binary printing the wired
pipeline packages; story 2.4 builds the real pipeline.
- `packages/viz` placeholder page — story 2.5 builds the GraphEngine;
story 4.1 turns the vite build into the single self-contained bundle.
113 changes: 113 additions & 0 deletions docs/dev/epic-1/1.2-contract-schema/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# 1.2-contract-schema — the contract, and how to change it

`@gitnebula/contract` holds the `analysis.json` JSON Schema, the TypeScript
types generated from it, and the validator every consumer runs. It is the only
interface between modules (AD-1) and it depends on nothing.

```bash
pnpm --filter @gitnebula/contract test # the schema's test suite
pnpm --filter @gitnebula/contract generate # regenerate src/generated/analysis.ts
```

## What the package exports

| export | what it is |
| --------------------------------- | --------------------------------------------------------- |
| `analysisSchema` | the raw draft 2020-12 schema, as JSON |
| `validateAnalysis(data)` | validator; returns a discriminated `ValidationResult` |
| `formatValidationErrors(errors)` | renders errors as an indented block for a cli message |
| `SUPPORTED_SCHEMA_MAJOR` | `1` — the major the Viewer accepts (AD-12) |
| `AnalysisDocument` and friends | **generated** types, from the schema |
| `ScanResult`/`DepsResult`/`GitResult`/`Config` | **hand-written** intermediate types (AD-1) |

`validateAnalysis` narrows on success, so a valid document is typed without a
cast:

```ts
const result = validateAnalysis(JSON.parse(raw));
if (!result.valid) {
throw new Error(`invalid analysis.json:\n${formatValidationErrors(result.errors)}`);
}
result.data.nodes; // AnalysisDocument["nodes"]
```

Errors address the offending value by JSON Pointer, and a missing property
points at the **absent field** (`/nodes/0/loc`) rather than at its parent
object — so a cli message can print the pointer verbatim.

## Two kinds of types, one rule

- **Contract types are generated.** `src/generated/analysis.ts` is written by
`scripts/generate-types.mjs` from the schema (AD-9). It carries a
do-not-edit banner, is excluded from ESLint, and CI regenerates it and runs
`git diff --exit-code` — a hand edit cannot survive a pull request.
- **Intermediate pipeline types are hand-written**, in `src/pipeline.ts`. They
are *derived* from the generated ones (`Pick<AnalysisNode, …>`) wherever they
carry contract fields, so a schema change propagates into them as a type
error instead of as silent drift.

## The schema-change protocol

The schema is the project's central artifact. Changing it is never a side
effect of another story (CLAUDE.md).

1. **Decide the version impact first.**
- *Additive and optional* — a new optional property, a widened nullable
(e.g. `description` going from `null` to `string | null` when the
describe layer lands): **minor** bump, `1.0` → `1.1`. Old readers keep
working; the `const` on `schemaVersion` becomes an enum of accepted
minors.
- *Anything a `1.0` reader would choke on* — a removed or renamed field, a
narrowed type, a new required property, a changed enum meaning:
**major** bump. That breaks every consumer.
2. **A major bump needs its own story and an ADR** (AD-9), and it must move
`SUPPORTED_SCHEMA_MAJOR` in lockstep, because the Viewer compares a loaded
document against it and shows the FR-6 mismatch screen rather than
rendering nonsense.
3. **Edit the schema, never the generated types.** Then run
`pnpm --filter @gitnebula/contract generate` and commit the regenerated
file in the same commit as the schema change.
4. **Update the fixtures** (story 1.3). They are checked against the schema,
so a change that forgets them fails their suite, not this one.
5. **Run `pnpm test && pnpm typecheck`** at the root, not just in this
package. The intermediate types are derived, so a contract change surfaces
as compile errors in scanner/deps/githist/cli.

## Deliberate shape decisions

These were settled here and are binding on the analyzers:

- **`additionalProperties: false` everywhere.** A typo in a producer is a
validation failure, not a silently dropped field.
- **`repo.stats.languages` is a share map** — language name → number in
`0..1`, not a byte or file count. The Viewer's legend wants shares.
- **`lastChangedAt` is nullable.** A zero-history node has no commit instant,
and analyzers may not read the clock (AD-13), so there is no fallback value.
- **`description`/`descriptionSource` are typed `null`**, required-present.
MVP has nothing that could populate them, and reserving the field as
null-only makes the describe layer's widening a minor, non-breaking act
(FR-8, AD-10).
- **Co-change `count` requires only `>= 1`.** The `>= 3` threshold and the
per-kind cap are tunable analyzer policy under the ≤ 5 MB budget (FR-7);
baking them into the frozen schema would turn a tuning change into a
version bump. githist enforces the policy.
- **`format: "date-time"` is registered locally** in `validate.ts` instead of
pulling in `ajv-formats`; it is the only format the contract uses. It checks
RFC 3339 *semantics*, not just digit placement: component ranges and the
calendar, so `2026-02-29` is rejected in a non-leap year and
`2026-99-99T25:61:61Z` cannot reach the Viewer as an `Invalid Date`. The
governing invariant, asserted in the tests: **every instant the contract
accepts parses with `new Date(...)`**, because that is what the Viewer does
with it. A leap second (`:60`) is therefore rejected even though RFC 3339
§5.6 permits one — git stores POSIX epoch seconds, which have no leap
second, and `new Date("2026-12-31T23:59:60Z")` is `Invalid Date`, so no
producer here can emit one and no consumer could read it.

## Environment neutrality (AC-5)

Nothing under `src/` may import a `node:` module or touch the DOM: `viz`
imports this package into the browser bundle, and `cli` imports it in Node.
The package's `tsconfig.json` sets `"types": []` with no DOM lib, so a
`node:` import fails to typecheck rather than failing at runtime in a
browser. `scripts/` is build tooling, outside the runtime path, and may use
Node freely.
Loading