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
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
26 changes: 25 additions & 1 deletion docs/guides/folder-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ 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 +136,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
15 changes: 15 additions & 0 deletions docs/spec/example-instance-tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ 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-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
1 change: 1 addition & 0 deletions docs/wiki/feature-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ 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 |
| 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
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
16 changes: 16 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 Down
13 changes: 12 additions & 1 deletion skills/ignatius-modeling/references/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@

### 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]

### 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
23 changes: 12 additions & 11 deletions src/app/components/entity/ExamplesAccordion.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ModelNode } from '../../../model/parse';
import { ExampleCell } from '../ui/ExampleCell';

export function ExamplesAccordion({ node, variant = 'modal' }: { node: ModelNode; variant?: 'modal' | 'dict' }) {
const examples = node.examples;
Expand All @@ -10,6 +11,15 @@ export function ExamplesAccordion({ node, variant = 'modal' }: { node: ModelNode
const headers = [...node.pk, ...declaredCols];
const isOpen = examples.length <= 3;

const cell = (row: Record<string, unknown>, h: string, emptyClassName: string) => (
<ExampleCell
value={row[h]}
columnType={node.columns[h]?.type}
label={`${node.id} · ${h}`}
emptyClassName={emptyClassName}
/>
);

if (variant === 'dict') {
return (
<details className="dict-examples" open={isOpen || undefined}>
Expand All @@ -23,11 +33,7 @@ export function ExamplesAccordion({ node, variant = 'modal' }: { node: ModelNode
{examples.map((row, i) => (
<tr key={i}>
{headers.map(h => (
<td key={h}>
{row[h] !== undefined && row[h] !== null
? String(row[h])
: <span className="dict-example-empty">–</span>}
</td>
<td key={h}>{cell(row, h, 'dict-example-empty')}</td>
))}
</tr>
))}
Expand All @@ -52,12 +58,7 @@ export function ExamplesAccordion({ node, variant = 'modal' }: { node: ModelNode
{examples.map((row, i) => (
<tr key={i}>
{headers.map(h => (
<td key={h}>
{row[h] !== undefined && row[h] !== null && row[h] !== ''
? String(row[h])
: <span className="example-empty">–</span>
}
</td>
<td key={h}>{cell(row, h, 'example-empty')}</td>
))}
</tr>
))}
Expand Down
6 changes: 2 additions & 4 deletions src/app/components/process/ProcessExamples.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FlowProcess, FlowExample } from '../../../flows/flow-parse';
import { ExampleCell } from '../ui/ExampleCell';

/**
* Renders the `examples: { in, out }` data from a FlowProcess as a series of
Expand Down Expand Up @@ -47,10 +48,7 @@ export function ProcessExamples({ examples }: { examples: FlowProcess['examples'
<tr key={ri}>
{cols.map(c => (
<td key={c}>
{row[c] !== undefined && row[c] !== null && row[c] !== ''
? String(row[c])
: <span className="example-empty">–</span>
}
<ExampleCell value={row[c]} label={c} emptyClassName="example-empty" />
</td>
))}
</tr>
Expand Down
26 changes: 26 additions & 0 deletions src/app/components/ui/ExampleCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { asJsonValue } from '../../logic/json-value';
import { JsonValue } from './JsonValue';

/**
* One cell of an example row, shared by the entity accordions and the process
* in/out tables so a structured value renders the same wherever it surfaces.
*
* `columnType` is the declared type when the caller has a column definition to
* hand; process example rows are free-form, so they omit it and only nested
* values are recognised.
*/
export function ExampleCell({ value, columnType, label, emptyClassName }: {
value: unknown;
columnType?: string;
label: string;
emptyClassName: string;
}) {
if (value === undefined || value === null || value === '') {
return <span className={emptyClassName}>–</span>;
}

const json = asJsonValue(value, columnType);
if (json !== null) return <JsonValue value={json} label={label} />;

return <>{String(value)}</>;
}
43 changes: 43 additions & 0 deletions src/app/components/ui/JsonValue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useState } from 'react';
import { describeJson, formatJsonFull, formatJsonPreview } from '../../logic/json-value';
import { Modal } from './Modal';

/**
* A structured value inside a table cell: monospaced one-line preview plus an
* expander that opens the pretty-printed document in its own dialog.
*
* The dialog is always `stacked`, which covers both callers without either of
* them having to know where it sits — from the dictionary page it is the only
* modal on screen, and from an entity or process dialog it layers over the
* opener. It renders inline rather than through a portal so that a click on its
* backdrop is caught by the opener's stop-propagation handler.
*/
export function JsonValue({ value, label }: { value: object; label: string }) {
const [expanded, setExpanded] = useState(false);

return (
<span className="json-value">
<code className="json-value-preview">{formatJsonPreview(value)}</code>
<button
type="button"
className="json-value-expand"
aria-label={`Expand ${label}`}
title={`Expand ${label}`}
onClick={() => setExpanded(true)}
>
</button>
{expanded && (
<Modal
title={label}
onClose={() => setExpanded(false)}
className="modal-json"
stacked
headerExtra={<span className="json-value-shape">{describeJson(value)}</span>}
>
<pre className="json-value-full"><code>{formatJsonFull(value)}</code></pre>
</Modal>
)}
</span>
);
}
Loading
Loading