Skip to content
Open
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
95 changes: 95 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/design/markdown-driven-erd.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ No attributes table here — that's generated from the frontmatter.

| Property | Required | Description |
|---|---|---|
| `type` | Yes | Logical type: text, integer, decimal, boolean, date, datetime, binary |
| `type` | Yes | Logical type: text, integer, decimal, boolean, date, datetime, binary, json |
| `nullable` | No | Default false. Opt-in only. |
| `default` | No | Logical default value (literal or function name like `now`). Shown in its own column in the UI. |
| `desc` | No | Brief purpose of this column — what function it serves, why it exists. Not a repeat of the column name. |
Expand Down
46 changes: 45 additions & 1 deletion docs/guides/folder-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,34 @@ A customer settles invoices with a [[PaymentMethod|payment method]] on file.
The target must match an entity id exactly (case-sensitive). A link to an entity that does not exist renders as muted, non-clickable text and is reported as a `body.unknown_link` finding, so a typo never passes silently. See [Validation and findings](validation.md).


### Code in the body


A tagged code fence is syntax-highlighted, in entity bodies and flow bodies alike:

~~~markdown
Selecting a party's usable methods:

```sql
select payment_method_id, type, label
from PaymentMethod
where party_id = @party_id;
```
~~~

Six languages ship with the viewer — `json`, `sql`, `javascript`, `typescript`, `python`, `bash` — along with their usual fence aliases (`js`, `ts`, `py`, `sh`, `shell`, `zsh`). An untagged fence, or one tagged with a language that is not bundled, renders as plain preformatted text rather than failing.

Highlighting is applied when the model is parsed, not in the browser, so a static `export` carries it with no extra work. Colours follow the viewer's own light/dark toggle.


### Columns


Each column takes a logical `type` and three optional fields.

| Field | Default | Meaning |
|---|---|---|
| `type` | required | One of `text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `binary` |
| `type` | required | One of `text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `binary`, `json` |
| `nullable` | `false` | Whether the column accepts null |
| `default` | none | A default value note |
| `desc` | none | A short note on what the column is for |
Expand Down Expand Up @@ -136,6 +156,30 @@ examples:
The rows render as a collapsible table in the entity dialog and the dictionary. Two or three realistic rows are enough; their job is to make the rules concrete — a sample row that violates a constraint you believe in reveals a modeling error no structural check can catch. Every key must be a real column (or PK column); the live server flags unknown keys with an `entity.example_unknown_column` warning.


### Structured values


A `json` column carries a document rather than a scalar. Write the value as nested YAML:

```yaml
columns:
details:
type: json
nullable: true
desc: "Instrument details, whose shape differs per method type."
examples:
- payment_method_id: 1
details:
network: visa
last4: "4471"
exp_month: 11
```

The cell renders as a truncated monospace preview with an expander beside it; the expander opens the pretty-printed document in a dialog, layered over the entity dialog when you are already in one. A nested value renders this way on any column, so an author who omits the `json` type still gets a readable cell — declaring it is what additionally lets a value written as a quoted JSON string be recognised as a document.

Reach for `json` only where the shape is genuinely open-ended. A fixed set of known fields is columns, and a repeating group is a child entity; a `json` column with a stable shape is a modeling miss the viewer cannot help you with.


## A group file


Expand Down
30 changes: 30 additions & 0 deletions docs/spec/example-instance-tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,36 @@ Live-only validation:

## Change log

### 2026-08-21 — Structured (`json`) example values

**What changed:** `json` (and its `jsonb` spelling) joins the column-type vocabulary, and an example cell holding a structured value now renders as a truncated monospace preview with an expander that opens the pretty-printed document in its own dialog. Added criteria:

- [ ] A cell whose value is a nested object or array renders via `JsonValue` on any declared type; a cell whose value is a *string* is parsed only when the column declares `json`/`jsonb`.
- [ ] The preview is compact single-line JSON clipped on a character budget (48 default), not a CSS ellipsis — these cells live in horizontally scrolling tables where full text sets the column width from the longest document.
- [ ] The expander's dialog is always `stacked`: from the dictionary it is the only dialog on screen, from an entity or process dialog it layers over the opener.
- [ ] `Modal` keeps a module-level stack and answers ESC only in the top-most instance, so a stacked dialog closes itself and leaves its opener open.
- [ ] Entity and process example tables share one `ExampleCell`, so a structured value renders identically on both surfaces.
- [ ] Formatting degrades rather than throwing on a self-referential value — YAML aliases can reach an ancestor node, and an exception inside a table cell takes the view down.

**Why:** `String(value)` rendered every nested value as `[object Object]`, so the frontmatter format accepted structured example data that no surface could display. The declared type is what licenses parsing a *string* as a document; recognising nested values unconditionally means an author who omits the type still gets a readable cell.

**Superseded:** the criterion "Missing values render as a muted en-dash" now also covers the empty string on the dict surface, which previously rendered `''` as an empty cell while the modal surface rendered the en-dash. One `ExampleCell` gives both the en-dash.

### 2026-08-21 — Syntax highlighting via shiki

**What changed:** the expanded JSON document is syntax-highlighted, and tagged code fences in entity and flow bodies are too. Added criteria:

- [ ] Highlighting uses shiki with `createHighlighterCoreSync` and `createJavaScriptRawEngine()` over precompiled grammars — never the default Oniguruma engine, whose WebAssembly payload would break both the single-file `export` and the zero-network guarantee.
- [ ] Two entry points, not one: `src/model/markdown-highlight.ts` (json, sql, javascript, typescript, python, bash) is reached only from `parse.ts` / `flow-parse.ts` and never crosses into the browser bundle; `src/app/logic/json-highlight.ts` loads json alone.
- [ ] Both run with `defaultColor: false`, so tokens carry `--shiki-light` and `--shiki-dark` and commit to neither; `styles.css` resolves them off the `.theme-*` root class so blocks follow the in-app toggle.
- [ ] `highlightCodeFence` returns `''` for an untagged fence, an unbundled language, or a grammar error, handing the block back to markdown-it's default escaping. A body must never fail to render over a code fence.
- [ ] Both producers escape HTML in the source; output is injected via `dangerouslySetInnerHTML` and the source is model data.
- [ ] The JSON document is highlighted only once its dialog opens — a dictionary page can hold hundreds of unexpanded cells.

**Why:** the split exists because grammars are static imports that no bundler can tree-shake. A single shared module would put roughly 620KB of grammar into the browser bundle; keeping the six-language set on the parse side holds the browser cost to about 140KB, since body markdown reaches the client as already-rendered `bodyHtml`. Measured: browser bundle 4.22MB → 4.36MB.

**Superseded:** the previous entry's criterion that the expanded document renders in a plain `<pre>` — it is now shiki's `<pre class="shiki">`, and `.json-value-full` is the scroll container around it rather than the styled block itself.

### 2026-05-31 — Re-number CP-5 step to E7b (was E5b)

**What changed:** the new examples step in the modeling skill is renamed from **E5b** to **E7b** and re-anchored as "between E7 (Columns) and E8 (Reference table)" instead of "between E5 (columns) and E6 (description)".
Expand Down
2 changes: 2 additions & 0 deletions docs/wiki/feature-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Paths are relative to `docs/design/`, `docs/spec/`, `docs/guides/`, and `skills/
| The modeling skill itself | ignatius-modeling-skill | ignatius-modeling-skill | modeling-skill | SKILL + all references |
| Skill `flow` + `discover` modes (DFD authoring; Socratic business→model discovery, five gates, generates entities + flows; reverse-engineering from existing DB/code/schema in the IDEF1X spirit) | noorm-flow-discovery | noorm-flow-discovery | modeling-skill, flows | SKILL (4-mode router), dfd-authoring, flow-templates, discover-flow, reverse-engineering |
| Example / sample instance tables | example-instance-tables | example-instance-tables | folder-format (example rows) | entity-flow E7b (`examples:` frontmatter) + templates |
| `json` column type + structured example cells (truncated monospace preview, expander opens the document in a dialog stacked over its opener; shared `ExampleCell` across entity and process tables) | markdown-driven-erd (type list) | example-instance-tables (change log) | folder-format (structured values) | conventions (type list + authoring note), entity-flow E7 |
| Syntax highlighting (shiki). Body code fences highlighted at parse time via markdown-it's `highlight` option — six grammars, server-side only; the expanded JSON document highlighted in the browser from a json-only module. Split deliberately: grammars are static imports and cannot be tree-shaken, so one shared module would put ~620KB in the bundle instead of ~140KB | — | example-instance-tables (change log) | folder-format (code in the body) | conventions (bundled languages) |
| SSADM process flows (DFD): parse, 11 `flow.*` rules, in-app Flows view (unified SPA), recursive data-level balancing, client-side drill-down, separate-key persistence, per-node ⓘ dialog + `[[wiki-link]]` routing; `db:` store opens rich entity dialog; process dictionary fused into Dictionary view ◆ | process-flows | process-flows (+ research `ssadm-dfd-rules`) | flows | — |
| Unified SPA collapse (Graph / Dictionary / Flows in one app; `export` replaces `dict`/`graph`/`flow`; fused searchable Dictionary; `db:` store → rich entity dialog; shared chrome + theme on DFDs) | unified-app | unified-app | commands, building-from-source | — |
| DFD polish round 2: no text-select on nodes (CP14); store/external coloring by `kind` (theme-aware, `theme.flowKinds` override) (CP15); per-process in/out data example tables (CP16) | — | dfd-polish-round2, process-flows (examples) | themes-and-branding (kind colors) | — |
Expand Down
3 changes: 2 additions & 1 deletion docs/wiki/parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ Reads a model root directory and produces a `Model`: entity nodes, edges, groups
## Coupling

- `validate` — two-way type coupling: `parse.ts` imports `GlobalError` from [`src/model/validate.ts`](../../src/model/validate.ts), and `validate.ts` imports `Model`/`ModelNode`/`ModelEdge`/`SubtypeCluster` from `parse.ts`. `model-index.ts` documents mirroring `validate.ts`'s `checkAlternateKeys` (AK column union) and `checkEdgeDanglingFkColumn` (FK column derivation from `edge.on` keys) logic — a change to either derivation must be kept in sync in both files.
- `flows` — [`src/flows/flow-validate.ts`](../../src/flows/flow-validate.ts) imports `Model` from `parse.ts`; [`src/flows/flow-parse.ts`](../../src/flows/flow-parse.ts) imports `wikiLinkPlugin` from `wikilink.ts` directly (its own markdown-it instance, separate from the one in `parse.ts`).
- `flows` — [`src/flows/flow-validate.ts`](../../src/flows/flow-validate.ts) imports `Model` from `parse.ts`; [`src/flows/flow-parse.ts`](../../src/flows/flow-parse.ts) imports `wikiLinkPlugin` from `wikilink.ts` directly (its own markdown-it instance, separate from the one in `parse.ts`). Both instances pass the same `highlight` callback, so entity and flow bodies highlight identically.
- [`src/model/markdown-highlight.ts`](../../src/model/markdown-highlight.ts) — shiki with six precompiled grammars (json, sql, javascript, typescript, python, bash) behind `createJavaScriptRawEngine()`, exported as `highlightCodeFence(code, lang)` and wired into both `MarkdownIt` constructors as the `highlight` option. Returns `''` on an untagged fence, an unbundled language, or a grammar throw, which hands the block back to markdown-it's default escaping. Server-side only: app code imports `parse.ts` for types alone, so neither markdown-it nor these grammars reach the browser bundle — the browser's own highlighter, [`src/app/logic/json-highlight.ts`](../../src/app/logic/json-highlight.ts), loads the json grammar by itself for the same reason.
- `frontend` ([`src/app/`](../../src/app)) — multiple modules under [`src/app/logic/`](../../src/app/logic) and [`src/app/hooks/`](../../src/app/hooks) import `Model`, `ModelNode`, `ModelEdge`, `Predicate`, `ThemeConfig`, or `SubtypeCluster` as types from `parse.ts`; `spotlight.ts` and `spotlight-inherited.ts` import `ModelIndex` from `model-index.ts`. `GroupConfig` is imported from `parse.ts` only outside `logic/`/`hooks/`, by [`src/app/App.tsx`](../../src/app/App.tsx), [`src/app/components/ui/FabMenu.tsx`](../../src/app/components/ui/FabMenu.tsx), and [`src/app/views/graph/styles.ts`](../../src/app/views/graph/styles.ts).
- `server` ([`src/server/server.ts`](../../src/server/server.ts)) and `cli` ([`src/cli/cli.ts`](../../src/cli/cli.ts)) both call `parseModels()` directly to produce the `Model` they serve or output.
- `generators` ([`src/generators/app.ts`](../../src/generators/app.ts)) imports the `Model` type from `parse.ts`.
Expand Down
2 changes: 1 addition & 1 deletion docs/wiki/skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Writes real files to disk and, after every write, runs the verification loop in
- [`skills/ignatius-modeling/references/flow-templates.md`](../../skills/ignatius-modeling/references/flow-templates.md) — the three DFD node file templates (process, external entity at `externals/<Name>.md`, non-`db` store at `stores/<slug>.md`) plus worked examples (`Collect-Payment.md`, `Customer.md`, `gateway-log.md`); states its frontmatter keys and endpoint tokens match [`docs/spec/process-flows.md`](../spec/process-flows.md).
- [`skills/ignatius-modeling/references/discover-flow.md`](../../skills/ignatius-modeling/references/discover-flow.md) — `discover` mode: verbs-first shape (find the verbs, derive the nouns, write nouns then verbs), the five gates (Identify, Decide, Justify, Derive, Ground) as plain-English questions, a `<constraints>` block banning the gates' underlying formal-logic names (excluded middle, law of identity, non-contradiction, sufficient reason, four causes, three-valued logic, falsifiable, syllogism, a priori, ontology) from ever reaching the user, and "crystallize as you go" incremental file-writing.
- [`skills/ignatius-modeling/references/reverse-engineering.md`](../../skills/ignatius-modeling/references/reverse-engineering.md) — extracting a model from an existing system (live DB/DDL, ORM models, codebase, stored procedures, API spec, sample data) in five IDEF1X-spirit phases R0–R4; explicitly faithful-first ("never silently 'fix' " an anti-pattern during extraction); feeds candidates back through `discover-flow.md`'s five gates.
- [`skills/ignatius-modeling/references/conventions.md`](../../skills/ignatius-modeling/references/conventions.md) — column type list (`text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `binary`), column-property table, and the classification/cardinality derivation tables the parser applies (read-only reference — the skill never asks the user for these).
- [`skills/ignatius-modeling/references/conventions.md`](../../skills/ignatius-modeling/references/conventions.md) — column type list (`text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `binary`, `json`), column-property table, and the classification/cardinality derivation tables the parser applies (read-only reference — the skill never asks the user for these).
- [`skills/ignatius-modeling/references/templates.md`](../../skills/ignatius-modeling/references/templates.md) — the entity `.md`, `groups/<slug>.md`, and `ignatius.yml` templates, plus worked key-inherited, orm-oriented, business-context, and subtype-cluster (base + member) examples.
- [`skills/ignatius-modeling/references/verification.md`](../../skills/ignatius-modeling/references/verification.md) — the `ignatius validate` loop: stderr line format, the full entity/parse/edge/cluster/body rule table and the `flow.*` rule table (each row mapped back to the authoring step that produced the finding), retry policy (max 5 attempts, prefilled re-ask rather than blind rewrite), and the post-clean-validate self-check (business context captured, predicates read as true sentences, example rows checked against `pk ∪ columns` since `entity.example_unknown_column` is live-server-only and `validate` never prints it).

Expand Down
25 changes: 25 additions & 0 deletions models/key-inherited/data/transactional/PaymentMethod.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,31 @@ columns:
label:
type: text
desc: "User-facing label (e.g. 'Visa ending 4242')."
details:
type: json
nullable: true
desc: "Instrument details, whose shape is set by the gateway and differs per type — no two method types carry the same fields."
examples:
- party_id: 2
payment_method_id: 1
type: CREDIT_CARD
label: Visa ending 4471
details:
network: visa
last4: "4471"
exp_month: 11
exp_year: 2028
billing_zip: "33139"
wallet: apple_pay
- party_id: 1
payment_method_id: 1
type: BANK_TRANSFER
label: Silicon Valley Bank ••1847
details:
account_type: checking
routing_last4: "0114"
account_last4: "1847"
verified_by: micro_deposits
- party_id: 3
payment_method_id: 1
type: CHECK
Expand All @@ -46,3 +62,12 @@ relationships:
A **PaymentMethod** is a means by which a Party can pay — a card, bank account, or check on file. It belongs to the party that holds it and is classified by a `PaymentMethodType`.

It exists as a stored, reusable record so a party can pay repeatedly without re-entering details, and so each `Payment` can point at exactly the instrument that settled it.

Selecting a party's usable methods:

```sql
select payment_method_id, type, label
from PaymentMethod
where party_id = @party_id
order by payment_method_id;
```
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
},
"dependencies": {
"@clack/prompts": "^1.5.0",
"@shikijs/langs-precompiled": "^4.4.3",
"@shikijs/themes": "^4.4.3",
"citty": "^0.2.2",
"cytoscape": "^3.31.0",
"cytoscape-elk": "^2.2.0",
Expand All @@ -36,6 +38,7 @@
"markdown-it": "^14.1.0",
"react": "^19",
"react-dom": "^19",
"shiki": "^4.4.3",
"web-worker": "^1.5.0",
"yaml": "^2.8.4"
},
Expand Down
21 changes: 20 additions & 1 deletion skills/ignatius-modeling/references/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,26 @@

### Column types

`text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `binary`
`text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `binary`, `json`

`json` holds a structured document. Reach for it only when the shape is genuinely open-ended — a fixed set of known fields is columns, and a repeating group is a child entity. A `json` column that turns out to have a stable shape is a modeling miss, not a shortcut.

Write `json` example values as nested YAML. A quoted JSON string parses too, but nested YAML is what the rest of the file already reads like:

examples:
- account_id: 1
settings:
theme: dark
digest: weekly
muted_tags: [billing, marketing]

### Code in bodies

A tagged code fence in an entity or flow body is syntax-highlighted by the viewer. Bundled languages:

`json`, `sql`, `javascript`, `typescript`, `python`, `bash` — plus the aliases `js`, `ts`, `py`, `sh`, `shell`, `zsh`

Tag every fence you write; an untagged one, or one tagged with a language outside that set, renders as plain preformatted text. Prefer `sql` for the query snippets that most often earn a place in an entity body.

### Column properties

Expand Down
2 changes: 1 addition & 1 deletion skills/ignatius-modeling/references/entity-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ If yes, collect: rule name + columns array. Repeat for each AK.

Ask: "List the non-PK columns. For each: name, type, nullable? (default false), optional default, optional desc."

Valid types: `text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `binary`
Valid types: `text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `binary`, `json`

Note: PK columns must also appear in `columns` with their types.

Expand Down
Loading
Loading