Skip to content

Bundles and concepts

A bundle is a directory of .md files that forms one specification. Loading a bundle walks that directory, decides what each file is from its name, parses the concepts, and indexes them into a graph.

Filename alone determines how a file is treated, in this order:

Filename Treated as In the graph?
index.md, log.md Reserved file No
<name>.<type-token>.md Concept Yes, as a node
Anything else, such as AGENTS.md Ignored No

A concept is one markdown file: YAML frontmatter plus a markdown body. Its ID is the bundle-relative path with .md removed, always with forward slashes:

specification/deliver-email.requirement.md -> specification/deliver-email.requirement

That ID is what graph --from takes and what appears in every JSON output.

The <type-token> segment is the kebab-case form of the frontmatter type:

login.requirement.md type: Requirement
auth.system.md type: System
checkout.user-journey.md type: UserJourney

If the token and the type disagree, validation warns. The full token list is in Node types.

index.md and log.md are reserved at every directory level. They carry no type, never become nodes, and are not counted as concepts. Both are generated by index: index.md lists the concepts in its directory grouped by type, and log.md at the bundle root accumulates dated entries.

Because they are generated, hand edits are reverted by the next index run.

Any other .md file with no .<type-token> segment is skipped. This is what lets AGENTS.md, README.md, or scratch notes live inside a bundle directory without becoming malformed concepts.

Skipped files are reported rather than silently dropped:

$ npx graph-spec-cli validate spec/
1 file(s) ignored (no type token): AGENTS.md
27 concept(s), 0 error(s), 0 warning(s)

validate --json carries the same list in an ignored array.

This rule is a deliberate departure from plain OKF, which treats every non-reserved file as a concept. GraphSpec uses the profile’s filename convention to decide membership so that docs and specs can share a directory.

Only type is structurally required, and it is the one field OKF itself demands. The profile adds required fields per type (status on Requirement, level on TestScenario, and so on).

Recommended on every concept:

---
type: Requirement
title: Deliver Email Notification
description: The system must email the user within one minute of a qualifying event.
status: accepted
tags: [notifications]
relations:
refines:
- /specification/notify.requirement.md
---

title and description are what index and query render, so a concept without them produces thin listings. Unknown frontmatter keys are preserved rather than rejected, per OKF’s extension rule.

Each type has conventional H1 sections (# Acceptance Criteria for Requirement, # Given/When/Then for TestScenario). These are not enforced, but following them keeps bundles legible to anyone who has read another one. The parser extracts H1 sections, so they are addressable.

There is none. Group concepts however the project wants. The product/, architecture/, specification/, glossary/ split used in examples is one convention, not a requirement.

Directories do create implicit parent to child structural edges, which graph --structure can include. Those are separate from typed relations and are excluded by default.

.git, node_modules, dist, and .vscode are skipped. A bundle nested inside one of them is invisible to the loader.