# 03 — The Data Model, from code

Prose descriptions of Issues-FS disagree with each other. **The code does not.** Build this page from the schema files.

Paths relative to `ifs-modules/Issues-FS`.

---

## 1. Node — `issues_fs/schemas/graph/Schema__Node.py`

```python
class Schema__Node(Type_Safe):                       # Base node structure for all entities
    node_id       : Node_Id                          # Random 10-char GUID for machine use
    node_type     : Safe_Str__Node_Type              # Classification: bug, task, feature, person
    node_index    : Safe_UInt                        # Per-type sequential number
    label         : Safe_Str__Node_Label             # Human-readable: "Bug-27", "Task-15"
    title         : Safe_Str__Text
    description   : Safe_Str__Issue__Node__Description
    status        : Safe_Str__Status                 # Type-specific
    created_at    : Timestamp_Now
    updated_at    : Timestamp_Now
    created_by    : Obj_Id                           # Person/agent node_id
    tags          : List[Safe_Str__Text]
    links         : List[Schema__Node__Link]         # Relationships
    properties    : Dict[str, Any]                   # Type-specific
```

**Two identities per node** — a machine `node_id` and a human `label` — is a design decision worth an explicit paragraph. It is why `issues-fs show Bug-27` works while the file on disk is addressed by GUID.

⚠️ The file carries five `# todo:` markers including *"refactor all these classes to use 'Issue'"* and *"this should be a Type_Safe collection and we shouldn't be using raw primitives like str, Any"*. Honest to mention; the `properties: Dict[str, Any]` field is the one the Type-Safe Properties brief (2,734 w) was written to fix and never did.

---

## 2. Edge — `issues_fs/schemas/graph/Schema__Node__Link.py`

```python
class Schema__Node__Link(Type_Safe):   # Links are bidirectional and denormalized (stored on both ends)
    link_type_id  : Obj_Id
    verb          : Safe_Str__Link_Verb              # "blocks", "has-task", "assigned-to"
    target_id     : Obj_Id
    target_label  : Safe_Str__Node_Label             # Denormalized for display
    created_at    : Timestamp_Now
```

**Edges are stored twice** — once on each endpoint, `verb` on one side and `inverse_verb` on the other. This is why the SGraph Send graph reports **141 link entries for ~70 logical relationships**. Explain that on the page; a reader who counts will otherwise think the numbers are wrong.

---

## 3. Type definitions

`Schema__Node__Type.py`: `type_id`, `name`, `display_name`, `description`, `icon`, `color` (`Safe_Str__Hex_Color`), `statuses: List[Safe_Str__Status]`, `default_status`, `properties: List[Schema__Property__Definition]`

`Schema__Link__Type.py`: `link_type_id`, `verb`, `inverse_verb`, `description`, **`source_types`**, **`target_types`**

Those last two are **domain/range constraints on an edge type** — the thing that makes this a typed property graph rather than a folder of JSON. Feature it.

---

## 4. The ID scheme — `issues_fs/schemas/graph/Safe_Str__Graph_Types.py`

**The single best teaching artefact in the codebase.** Five regex-validated primitives, all `Enum__Safe_Str__Regex_Mode.MATCH` with `strict_validation = True` — invalid input raises rather than coerces:

| Primitive | Regex | Max | Example |
|---|---|---|---|
| `Safe_Str__Node_Type` | `^[a-z][a-z0-9-]*$` | 50 | `bug`, `git-repo` |
| `Safe_Str__Node_Label` | `^[A-Z][a-zA-Z]*(-[A-Z][a-zA-Z]*)*-\d{1,5}$` | 100 | `Bug-27`, `Git-Repo-1`, `User-Story-1` |
| `Safe_Str__Status` | `^[a-z][a-z0-9-]*$` | 50 | `in-progress` |
| `Safe_Str__Link_Verb` | `^[a-z][a-z0-9-]*$` | 50 | `blocks`, `has-task` |
| `Safe_Str__Node_Type_Display` | `^[A-Z][a-zA-Z0-9 ]*$` | 100 | `Bug`, `User Story` |

Label generation, `Path__Handler__Graph_Node.label_from_type_and_index()`:
```python
display_type = node_type.capitalize()                          # "bug" → "Bug"
return Safe_Str__Node_Label(f"{display_type}-{node_index}")    # "Bug-27"
```

Others: `Safe_Str__Hex_Color` (`^#[0-9A-Fa-f]{6}$`) · `Safe_Str__Issue_Id` (`^[Ii]ssue-\d{1,5}$`) · `Safe_Str__Issue__Node__Description` (max **50,000**, REPLACE mode, preserves `\n\r\t`, allows empty).

⚠️ `issues_fs/schemas/identifiers/Issue_Id.py` carries the comment *"we need to refactor the code to use this"* — it is not yet used by `Schema__Node`.

---

## 5. On-disk layout

The `SGraph-AI__App__Send` graph is the reference specimen: **fully hierarchical**, containment expressed as directory nesting.

```
.issues/
├── config/
│   ├── node-types.json          ← 12 node types
│   └── link-types.json          ← 10 verb/inverse pairs with domain/range
└── issues/
    └── Project-1/
        └── issues/
            └── Phase-1/
                └── issues/
                    └── Feature-9/
                        └── issues/
                            └── Task-N/
                                └── issue.json
```

Max depth **8 path segments**; 55 of the 71 nodes sit at that depth. 84 files total.

⚠️ **Three incompatible layouts are in circulation** across the ecosystem: hierarchical `issues/` nesting (Send), flat `data/` (Issues-FS, CLI), and mixed (Service__UI: 44 `data/`, 4 `issues/`, 1 root). The docs describe a fourth. **Pick one for the site, show the others as "also seen", and say which the CLI actually produces.**

⚠️ **`_index.json` is a cache, not a source.** `Issues-FS/.issues/_index.json` declares 22 issues and 6 bugs; on disk there are 24 and 8. The Send graph has **no `_index.json` at any level** and works fine, because the CLI walks the tree. That is a good honest argument, not an embarrassment — make it explicitly.

---

## 6. Storage backends

`issues_fs/schemas/enums/Enum__Graph__Storage__Backend.py` — `MEMORY`, `LOCAL_DISK`, `SQLITE`, `ZIP`. All four real, all constructed through `Graph__Repository__Factory` over the memory-fs abstraction.

```python
repo = Graph__Repository__Factory.create_local_disk(root_path=".issues")
repo = Graph__Repository__Factory.create_sqlite(db_path="issues.db")
```

⚠️ **S3 does not exist** despite being claimed in four documents. See `07__gaps-and-open-questions.md`.

---

## 7. The 12 node types and 10 link types, as shipped

From the live config in `SGraph-AI__App__Send/.issues/config/` — publish these JSON files as a download; they are the cheapest credibility on the site.

**Node types:** `git-repo` · `bug` · `task` · `feature` · `person` · `project` · `phase` · `research` · `spike` · `security-review` · `threat-model` · `question`

**Link types (verb / inverse):** `blocks`/`blocked-by` · `has-task`/`task-of` · `assigned-to`/`assignee-of` · `depends-on`/`dependency-of` · `relates-to`/`relates-to` · `contains`/`contained-by` · `has-project`/`project-of` · `has-phase`/`phase-of` · `has-feature`/`feature-of` · `asks`/`asked-by`

⚠️ **`relates-to`/`relates-to` is a self-inverse pair**, and the wider corpus explicitly forbids it — *"You can never have relates-to, because relates-to is meaningless, two things always relate to each other."* One edge instance in the live graph uses it. **Narrate the tension rather than hiding it**; it is exactly the kind of honest detail the sibling sites are built on, and graphs.sgit.ai already makes the argument.

---

This document is released under the Creative Commons Attribution 4.0 International licence (CC BY 4.0).
