DATA MODEL · data-model.md
Data Model: speckit-eye
Raw markdown
# Data Model: speckit-eye
**Feature**: [spec.md](./spec.md) | **Research**: [research.md](./research.md)
The model is built in memory from the project's files on every scan. Nothing is saved anywhere (the spec has no progress store). All paths below are POSIX-style and relative to the project root.
## Overview
```text
Project
├── constitution?: Artifact
├── assessments: Assessment[] ── artifacts: Artifact[]
├── features: Feature[] (folder order)
│ ├── artifacts: Artifact[]
│ ├── stories: Story[] (from spec.md)
│ ├── phases: Phase[] (from tasks.md)
│ │ ├── groups: StoryGroup[] (only when the phase mixes stories)
│ │ └── tasks: Task[]
│ └── warnings: Warning[]
├── active: ActiveSelection
├── totals: Totals
└── warnings: Warning[] (project-level)
```
## Entities
### Project
| Field | Type | Rule / source |
|---|---|---|
| `name` | string | Base name of the project folder (FR-015). |
| `root` | string | Absolute path. Used only by the reader; never rendered. |
| `features` | Feature[] | One per direct subfolder of `specs/`, sorted by folder name with a plain string compare (FR-008). |
| `constitution` | Artifact \| null | `.specify/memory/constitution.md` if it exists. |
| `assessments` | Assessment[] | One per direct subfolder of `.specify/assessments/`, sorted by name. |
| `active` | ActiveSelection | See the rules below (FR-014). |
| `totals` | Totals | Aggregated over `features` (FR-010, FR-015). |
| `warnings` | Warning[] | Problems not tied to a single feature (for example an invalid `feature.json`). |
**Validation**: the folder is a Spec Kit project if `specs/` or `.specify/` exists; otherwise the CLI exits with code 2 (see [contracts/cli.md](./contracts/cli.md)).
### Feature
| Field | Type | Rule / source |
|---|---|---|
| `dir` | string | Folder name, for example `001-speckit-eye-dashboard`. Unique; used as the key. |
| `title` | string | The `# Feature Specification: X` heading of `spec.md` if present, otherwise `dir`. |
| `artifacts` | Artifact[] | Every `*.md` under `specs/<dir>/`, recursively (FR-021). Ordering: see Artifact. |
| `stories` | Story[] | Parsed from `spec.md` (may be empty). |
| `phases` | Phase[] | Parsed from `tasks.md` (empty if the file is missing). |
| `hasTasks` | boolean | `tasks.md` exists. |
| `counts` | Counts | Sum over all tasks, including unphased ones. |
| `stage` | Stage | Derived; see State below. |
| `warnings` | Warning[] | From parsing this feature's files. |
### Phase
| Field | Type | Rule / source |
|---|---|---|
| `number` | integer \| null | From `## Phase N: …`; `null` for the synthetic "Unphased" phase (tasks before the first phase heading, with a warning). |
| `title` | string | Heading text after `N:`. |
| `tasks` | Task[] | In file order. |
| `storyLabels` | string[] | Distinct `USn` labels among the tasks, in order of first appearance. |
| `mergedStory` | Story \| null | Set when every task carries the same single label (FR-015 merged row). |
| `groups` | StoryGroup[] | Only when `storyLabels.length > 1`: one group per label. Unlabeled tasks stay directly on the phase. |
| `counts` | Counts | |
| `key` | string | `<featureDir>/p<number or 'u'>`, used for `data-key` in the DOM. When the same phase number appears more than once in a file, every occurrence gets `@L<line>` appended (for example `001-x/p3@L40`), so keys stay unique. |
### Story (from spec.md)
| Field | Type | Rule / source |
|---|---|---|
| `label` | string | `US<n>` from `### User Story <n> - <title> (Priority: P<k>)`. |
| `title` | string | |
| `priority` | string \| null | `P1`, `P2`, … |
A `[USn]` label with no matching Story is still shown under its label, with a warning (spec edge case).
### StoryGroup
| Field | Type |
|---|---|
| `label` | string |
| `story` | Story \| null |
| `tasks` | Task[] |
| `counts` | Counts |
| `key` | `<phaseKey>/<label>` |
### Task
| Field | Type | Rule / source |
|---|---|---|
| `id` | string \| null | `T\d+`; `null` for a checkbox line without an ID (counted, with a warning). |
| `done` | boolean | `[x]` or `[X]` → true; `[ ]` → false. |
| `parallel` | boolean | `[P]` marker present. |
| `story` | string \| null | `USn` label. |
| `description` | string | The rest of the line with the markers removed. |
| `dependsOn` | string[] | IDs from `depends on T012, T013` (FR-015c). IDs that are unknown in the same file are dropped, with a warning. |
| `line` | integer | 1-based source line (for warnings and for the tasks.md link). |
| `state` | TaskState | Derived; see below. |
| `key` | string | `<featureDir>/<id or 'L'+line>`; when the ID is duplicated in the file, every occurrence gets `@L<line>` appended (for example `001-x/T012@L57`). |
**Uniqueness**: task IDs should be unique within one `tasks.md`. Duplicates are all counted, and a warning is raised (W5). Keys are always unique (see `key`), so each duplicate is its own DOM item and at most one task is `current`.
### Artifact
| Field | Type | Rule / source |
|---|---|---|
| `kind` | enum | `spec`, `plan`, `research`, `data-model`, `quickstart`, `tasks`, `contract`, `checklist`, `constitution`, `assessment`, `other` |
| `title` | string | The first `# ` heading, otherwise the file name. |
| `source` | string | Project-relative path (for example `specs/001-x/contracts/cli.md`). |
| `url` | string | Page path without the base path (see [contracts/routes.md](./contracts/routes.md)). |
**Ordering inside a feature**: spec, plan, research, data-model, quickstart, tasks, contracts (by name), checklists (by name), then the rest by path (FR-021).
### Assessment
| Field | Type |
|---|---|
| `slug` | folder name under `.specify/assessments/` |
| `artifacts` | Artifact[]: its `*.md` files; intake, research, problem, concept, decision first, then the rest by name |
### Warning
| Field | Type |
|---|---|
| `file` | project-relative path |
| `line` | integer \| null |
| `message` | string (a short plain-language sentence) |
Warnings are shown on the feature in the tree (FR-019), and serve mode and build mode print them to stderr as `warning: <file>:<line> <message>`.
### Counts / Totals
- `Counts = { done, total, open = total - done, percent = total ? min(round(done*100/total), open ? 99 : 100) : 0 }`. Rounded to the nearest whole number (40/65 → 62 %, as in spec US1), but capped at 99 % while any task is open.
- `Totals = { tasks: Counts, specs: {completed, total}, phases: {completed, total} }`
- a spec is completed when `hasTasks && counts.total > 0 && counts.open == 0`; every feature counts toward `specs.total`.
- a phase is completed when `counts.total > 0 && counts.open == 0`; only phases with `counts.total > 0` count toward `phases.total`.
### ActiveSelection (FR-014)
| Field | Type |
|---|---|
| `featureDir` | string \| null |
| `phaseKey` | string \| null |
| `storyLabel` | string \| null (only when the active phase has groups) |
| `nextTaskKey` | string \| null |
| `source` | `feature.json` \| `git-branch` \| `first-open` \| `none` |
**Rules**:
1. Candidate A is `basename(feature.json.feature_directory)`, if the file parses and names an existing feature.
2. Candidate B is the current branch from the git `HEAD` file (`.git/HEAD`, or `HEAD` in a worktree's gitdir, research R9), if it equals an existing feature's `dir`.
3. If at least one feature has `open > 0`: a candidate is skipped when `hasTasks && open == 0`. If both are skipped or missing, the active feature is the first feature in order with `open > 0` (`source = first-open`).
4. If no feature has `open > 0` (all complete, FR-018): candidate A, else candidate B, is the active feature even when it is complete, with `phaseKey = storyLabel = nextTaskKey = null`. It is expanded, highlighted, and keeps its own status style (a complete feature is green with the done mark). If neither candidate exists, `featureDir = null` and `source = none`. The "all tasks are complete" message depends on `totals.tasks.total > 0 && totals.tasks.open == 0`, not on `source`.
5. Within the active feature: the active phase is the first phase with `open > 0`; if it has groups, the active story is the first group with `open > 0`; the next task is the first open task in the active group, or in the phase when it has no groups. (Unlabeled tasks in a mixed phase come before the groups only if they come first in file order; file order decides.)
6. A feature without `tasks.md` can be active, with `phaseKey = nextTaskKey = null`.
## State
### Stage (feature, FR-012 / FR-019a)
| Stage | Condition | Tree style |
|---|---|---|
| `empty` | no `spec.md`, no `tasks.md` | not started (dark grey) |
| `specified` | `spec.md`, no `plan.md`, no `tasks.md` | not started |
| `planned` | `plan.md`, no `tasks.md` | not started |
| `ready` | `tasks.md`, `done == 0`, `total > 0` | not started |
| `in-progress` | `tasks.md`, `0 < done < total` | started (blue) |
| `complete` | `tasks.md`, `total > 0`, `open == 0` | done (green, check mark, collapsed unless it is the active feature, FR-018) |
A feature whose `tasks.md` has no tasks at all (`total == 0`) stays `planned`, or `specified`/`empty`, and gets a warning. The active feature is additionally styled as highlighted, whatever its stage.
In the DOM, a feature's `data-status` is `done` (complete), `started` (in-progress), or `not-started` (every other stage); the active feature carries `data-active` in addition, so a complete active feature is both green and highlighted.
Phases and story groups use the same not-started / started / done styles, based on their counts.
### TaskState (FR-015b)
```text
done ──────────────────────────────► completed
open, key == active.nextTaskKey ───► current
open, any dependsOn task open ─────► blocked
open, otherwise ───────────────────► future
```
Rules are checked in this order, so the next task is shown as `current` even if a dependency is open (with a warning, per the spec's edge case).
## Change signature (live updates)
Every rendered tree node and grid cell carries `data-key` (stable identity) and `data-sig` (a short string built from the counts, the stage or state, and whether it is active). After a live swap, the client highlights the elements whose `data-sig` differs from the previous one for the same `data-key` (FR-028). The details are in [contracts/routes.md](./contracts/routes.md).
Feature: spec.md | Research: research.md
The model is built in memory from the project's files on every scan. Nothing is saved anywhere (the spec has no progress store). All paths below are POSIX-style and relative to the project root.
01 Overview
Project
├── constitution?: Artifact
├── assessments: Assessment[] ── artifacts: Artifact[]
├── features: Feature[] (folder order)
│ ├── artifacts: Artifact[]
│ ├── stories: Story[] (from spec.md)
│ ├── phases: Phase[] (from tasks.md)
│ │ ├── groups: StoryGroup[] (only when the phase mixes stories)
│ │ └── tasks: Task[]
│ └── warnings: Warning[]
├── active: ActiveSelection
├── totals: Totals
└── warnings: Warning[] (project-level)
02 Entities
Project
| Field | Type | Rule / source |
|---|---|---|
name |
string | Base name of the project folder (FR-015). |
root |
string | Absolute path. Used only by the reader; never rendered. |
features |
Feature[] | One per direct subfolder of specs/, sorted by folder name with a plain string compare (FR-008). |
constitution |
Artifact | null | .specify/memory/constitution.md if it exists. |
assessments |
Assessment[] | One per direct subfolder of .specify/assessments/, sorted by name. |
active |
ActiveSelection | See the rules below (FR-014). |
totals |
Totals | Aggregated over features (FR-010, FR-015). |
warnings |
Warning[] | Problems not tied to a single feature (for example an invalid feature.json). |
Validation: the folder is a Spec Kit project if specs/ or .specify/ exists; otherwise the CLI exits with code 2 (see contracts/cli.md).
Feature
| Field | Type | Rule / source |
|---|---|---|
dir |
string | Folder name, for example 001-speckit-eye-dashboard. Unique; used as the key. |
title |
string | The # Feature Specification: X heading of spec.md if present, otherwise dir. |
artifacts |
Artifact[] | Every *.md under specs/<dir>/, recursively (FR-021). Ordering: see Artifact. |
stories |
Story[] | Parsed from spec.md (may be empty). |
phases |
Phase[] | Parsed from tasks.md (empty if the file is missing). |
hasTasks |
boolean | tasks.md exists. |
counts |
Counts | Sum over all tasks, including unphased ones. |
stage |
Stage | Derived; see State below. |
warnings |
Warning[] | From parsing this feature's files. |
Phase
| Field | Type | Rule / source |
|---|---|---|
number |
integer | null | From ## Phase N: …; null for the synthetic "Unphased" phase (tasks before the first phase heading, with a warning). |
title |
string | Heading text after N:. |
tasks |
Task[] | In file order. |
storyLabels |
string[] | Distinct USn labels among the tasks, in order of first appearance. |
mergedStory |
Story | null | Set when every task carries the same single label (FR-015 merged row). |
groups |
StoryGroup[] | Only when storyLabels.length > 1: one group per label. Unlabeled tasks stay directly on the phase. |
counts |
Counts | |
key |
string | <featureDir>/p<number or 'u'>, used for data-key in the DOM. When the same phase number appears more than once in a file, every occurrence gets @L<line> appended (for example 001-x/p3@L40), so keys stay unique. |
Story (from spec.md)
| Field | Type | Rule / source |
|---|---|---|
label |
string | US<n> from ### User Story <n> - <title> (Priority: P<k>). |
title |
string | |
priority |
string | null | P1, P2, … |
A [USn] label with no matching Story is still shown under its label, with a warning (spec edge case).
StoryGroup
| Field | Type |
|---|---|
label |
string |
story |
Story | null |
tasks |
Task[] |
counts |
Counts |
key |
<phaseKey>/<label> |
Task
| Field | Type | Rule / source |
|---|---|---|
id |
string | null | T\d+; null for a checkbox line without an ID (counted, with a warning). |
done |
boolean | [x] or [X] → true; [ ] → false. |
parallel |
boolean | [P] marker present. |
story |
string | null | USn label. |
description |
string | The rest of the line with the markers removed. |
dependsOn |
string[] | IDs from depends on T012, T013 (FR-015c). IDs that are unknown in the same file are dropped, with a warning. |
line |
integer | 1-based source line (for warnings and for the tasks.md link). |
state |
TaskState | Derived; see below. |
key |
string | <featureDir>/<id or 'L'+line>; when the ID is duplicated in the file, every occurrence gets @L<line> appended (for example 001-x/T012@L57). |
Uniqueness: task IDs should be unique within one tasks.md. Duplicates are all counted, and a warning is raised (W5). Keys are always unique (see key), so each duplicate is its own DOM item and at most one task is current.
Artifact
| Field | Type | Rule / source |
|---|---|---|
kind |
enum | spec, plan, research, data-model, quickstart, tasks, contract, checklist, constitution, assessment, other |
title |
string | The first # heading, otherwise the file name. |
source |
string | Project-relative path (for example specs/001-x/contracts/cli.md). |
url |
string | Page path without the base path (see contracts/routes.md). |
Ordering inside a feature: spec, plan, research, data-model, quickstart, tasks, contracts (by name), checklists (by name), then the rest by path (FR-021).
Assessment
| Field | Type |
|---|---|
slug |
folder name under .specify/assessments/ |
artifacts |
Artifact[]: its *.md files; intake, research, problem, concept, decision first, then the rest by name |
Warning
| Field | Type |
|---|---|
file |
project-relative path |
line |
integer | null |
message |
string (a short plain-language sentence) |
Warnings are shown on the feature in the tree (FR-019), and serve mode and build mode print them to stderr as warning: <file>:<line> <message>.
Counts / Totals
Counts = { done, total, open = total - done, percent = total ? min(round(done*100/total), open ? 99 : 100) : 0 }. Rounded to the nearest whole number (40/65 → 62 %, as in spec US1), but capped at 99 % while any task is open.Totals = { tasks: Counts, specs: {completed, total}, phases: {completed, total} }- a spec is completed when
hasTasks && counts.total > 0 && counts.open == 0; every feature counts towardspecs.total. - a phase is completed when
counts.total > 0 && counts.open == 0; only phases withcounts.total > 0count towardphases.total.
- a spec is completed when
ActiveSelection (FR-014)
| Field | Type |
|---|---|
featureDir |
string | null |
phaseKey |
string | null |
storyLabel |
string | null (only when the active phase has groups) |
nextTaskKey |
string | null |
source |
feature.json | git-branch | first-open | none |
Rules:
- Candidate A is
basename(feature.json.feature_directory), if the file parses and names an existing feature. - Candidate B is the current branch from the git
HEADfile (.git/HEAD, orHEADin a worktree's gitdir, research R9), if it equals an existing feature'sdir. - If at least one feature has
open > 0: a candidate is skipped whenhasTasks && open == 0. If both are skipped or missing, the active feature is the first feature in order withopen > 0(source = first-open). - If no feature has
open > 0(all complete, FR-018): candidate A, else candidate B, is the active feature even when it is complete, withphaseKey = storyLabel = nextTaskKey = null. It is expanded, highlighted, and keeps its own status style (a complete feature is green with the done mark). If neither candidate exists,featureDir = nullandsource = none. The "all tasks are complete" message depends ontotals.tasks.total > 0 && totals.tasks.open == 0, not onsource. - Within the active feature: the active phase is the first phase with
open > 0; if it has groups, the active story is the first group withopen > 0; the next task is the first open task in the active group, or in the phase when it has no groups. (Unlabeled tasks in a mixed phase come before the groups only if they come first in file order; file order decides.) - A feature without
tasks.mdcan be active, withphaseKey = nextTaskKey = null.
03 State
Stage (feature, FR-012 / FR-019a)
| Stage | Condition | Tree style |
|---|---|---|
empty |
no spec.md, no tasks.md |
not started (dark grey) |
specified |
spec.md, no plan.md, no tasks.md |
not started |
planned |
plan.md, no tasks.md |
not started |
ready |
tasks.md, done == 0, total > 0 |
not started |
in-progress |
tasks.md, 0 < done < total |
started (blue) |
complete |
tasks.md, total > 0, open == 0 |
done (green, check mark, collapsed unless it is the active feature, FR-018) |
A feature whose tasks.md has no tasks at all (total == 0) stays planned, or specified/empty, and gets a warning. The active feature is additionally styled as highlighted, whatever its stage.
In the DOM, a feature's data-status is done (complete), started (in-progress), or not-started (every other stage); the active feature carries data-active in addition, so a complete active feature is both green and highlighted.
Phases and story groups use the same not-started / started / done styles, based on their counts.
TaskState (FR-015b)
done ──────────────────────────────► completed
open, key == active.nextTaskKey ───► current
open, any dependsOn task open ─────► blocked
open, otherwise ───────────────────► future
Rules are checked in this order, so the next task is shown as current even if a dependency is open (with a warning, per the spec's edge case).
04 Change signature (live updates)
Every rendered tree node and grid cell carries data-key (stable identity) and data-sig (a short string built from the counts, the stage or state, and whether it is active). After a live swap, the client highlights the elements whose data-sig differs from the previous one for the same data-key (FR-028). The details are in contracts/routes.md.