Metadata-Version: 2.4
Name: note-state
Version: 0.2.0
Summary: A portable state-tracking layer for markdown note vaults (Obsidian, Dendron, Foam, and plain markdown folders).
Author-email: superxgen <superxgen@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/resystem0/note-state
Project-URL: Issues, https://github.com/resystem0/note-state/issues
Keywords: notes,markdown,obsidian,pkm,zettelkasten,frontmatter,state-machine
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Text Processing :: Markup :: Markdown
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: PyYAML>=6.0
Dynamic: license-file

# note-state

A portable "patch" for turning a pile of notes into a *pipeline*: every note
carries an explicit `state` telling you how far it's traveled from raw
capture to integrated, trustworthy knowledge — the same way a ticket in an
issue tracker carries a status, or a part on an assembly line carries a
stage.

Works with any note application that stores notes as markdown files with
**YAML frontmatter** on local disk — Obsidian, Dendron, Foam, Logseq, plain
markdown vaults, even a folder of `.md` files with no app at all.

## Install

```bash
pip install note-state
```

## The pipeline

```
drafting → review → formatting → referencing → integrating → published
                ↘         ↘            ↘             ↘
                          needs_revision  (loops back to whichever
                                           stage flagged the problem)

              any state ──────────────────────────────→ archived
```

| State | What happens here | Exit condition |
|---|---|---|
| **Drafting** | Raw capture. Get the idea down with as little friction as possible. | The idea has enough substance to be judged. |
| **Review** | Read it critically: accurate, internally consistent, complete? | Content is verified, or it's sent back to Drafting. |
| **Formatting** | Give it shape: headings, atomic scope, a real title, clean prose, tags. | It reads well and is scoped correctly. |
| **Referencing** | Attach provenance: citations, source links, backlinks. | Every non-obvious claim is traceable to a source. |
| **Integrating** | Weave it into the graph: link from a Map of Content, cross-link related notes. | It's reachable from at least one other place in the vault. |
| **Published** | Stable and reusable — safe to cite, export, or share. | Terminal, but can be pulled back into Integrating. |
| **Needs Revision** *(utility state)* | Something's wrong. Flagged from any stage. | Always exits back to the stage that flagged it. |
| **Archived** *(utility state)* | Retired or superseded. | Terminal. |

This isn't sacred — it's just the bundled default. Drop your own
`states.yaml` next to your notes (or in any parent directory) to define a
different pipeline; `note-state` doesn't hardcode the states, it just
enforces whatever pipeline you describe.

## The metadata schema

Every tracked note gets two frontmatter fields: the current state, and a
timestamped log of how it got there.

```yaml
---
title: "Maps of Content connect clusters of related notes"
state: referencing
state_history:
  - state: drafting
    date: 2026-07-01
  - state: review
    date: 2026-07-05
  - state: formatting
    date: 2026-07-08
  - state: referencing
    date: 2026-07-12
    note: "added three sources"
tags: [pkm, moc]
---

Note body goes here as normal — nothing about the state system touches
the content itself.
```

`state_history` is what makes the system useful beyond a label: it's an
audit trail you can query later ("which notes have been stuck in Review for
three weeks?", "how long does formatting usually take?").

## Usage

```bash
# Stamp every note in a folder with the initial state (only touches
# notes that don't already have one — safe to run repeatedly)
note-state init ./my-vault

# Move a note forward
note-state set my-vault/atomic-notes.md review

# Try to skip a stage — this is rejected unless you pass --force
note-state set my-vault/atomic-notes.md published

# Move many notes at once: multiple paths, a glob, or a list piped over stdin
note-state set a.md b.md c.md review
note-state set --glob "my-vault/**/*.md" review
find my-vault -name "*.md" -mtime -1 | note-state set review --stdin
# (each file is validated independently — one illegal transition doesn't
# block the rest, but the command still exits non-zero if any failed)

# See what's where
note-state list ./my-vault
note-state list ./my-vault --state review

# Find bottlenecks — counts per state + oldest untouched notes
note-state report ./my-vault

# CI / pre-commit friendly: exit non-zero if anything has sat too long
note-state check ./my-vault --max-age 14
note-state check ./my-vault --max-age 3 --state review --state formatting

# Print the pipeline as a Mermaid diagram (paste into anything that renders it)
note-state graph
```

`list`, `report`, and `check` cache their per-note results (in a
`.note-state-cache.json` file dropped next to the notes, keyed by each
file's mtime) so re-running them on a large vault only re-parses notes that
actually changed since the last run. Safe to `.gitignore`; it's rebuilt
automatically.

## Customizing the pipeline

Two files define the state machine:

- **`states.yaml`** — states + allowed transitions. `note-state` looks for
  this file (or `.note-states.yaml`) in the current directory, then any
  parent directory, and falls back to the bundled default pipeline above if
  none is found. Copy the bundled default as a starting point and edit
  freely — rename stages, add a `translating` or `peer-review` state,
  rewire transitions.
- Pass `--config path/to/states.yaml` to point at a specific file instead
  of relying on directory discovery.

### Shortcuts (trapdoor transitions without `--force`)

Add a top-level `shortcuts:` list to allow specific transitions from *any*
state (or from one named state) without requiring `--force` every time:

```yaml
shortcuts:
  - to: needs_revision      # any state -> needs_revision, always allowed
  - from: published
    to: archived            # only published -> archived is shortcut-allowed
```

The bundled default ships with `any state -> needs_revision`, since flagging
a problem shouldn't require fighting the tool.

### Multiple pipelines (different note types, one vault)

If different kinds of notes should follow different pipelines — say, meeting
notes shouldn't go through the same stages as permanent notes — define a
`pipelines:` map instead of a single top-level pipeline:

```yaml
pipelines:
  default:
    initial_state: drafting
    states: { drafting: {}, review: {}, published: {} }
    transitions:
      drafting: [review]
      review: [published, drafting]
      published: []
  meeting:
    initial_state: captured
    states: { captured: {}, actioned: {}, archived: {} }
    transitions:
      captured: [actioned]
      actioned: [archived]
      archived: []
```

A note picks its pipeline via a `type:` frontmatter field (`type: meeting`);
notes with no `type` use `default`. `note-state init --type meeting ...`
stamps that type onto notes that don't already declare one. `note-state
graph` prints every pipeline unless you pass `--pipeline NAME`.

## Wiring it into a specific app

- **Obsidian, Dendron, Foam, plain markdown folders** — these already read
  and preserve YAML frontmatter, so `note-state` works with zero
  adaptation: run it straight against your vault folder. In Obsidian, the
  [Dataview](https://github.com/blacksmithgu/obsidian-dataview) plugin can
  turn `state` into a live board, e.g.:

  ```
  TABLE state, state_history[-1].date AS "last moved"
  FROM "Notes"
  SORT state
  ```

- **Logseq** — pages are markdown files too, but Logseq's own metadata
  lives as a `key:: value` line on the first block rather than a `---`
  frontmatter block. Keep frontmatter *and* Logseq properties side by side
  (Logseq ignores the frontmatter block, `note-state` ignores the `::`
  line), or adapt the parser in `note_state/cli.py` to read
  `state:: drafting` instead.

- **Notion** (or any database-backed app without local files) — recreate
  `states.yaml` as a **Status** property with matching options, and use a
  Board view grouped by it. You lose `note-state`'s enforcement of
  transition rules unless you script it against the Notion API, but the
  state vocabulary and pipeline shape carry over directly.

## Design notes

- **Backward moves are allowed on purpose.** Review can send a note back to
  Drafting; Formatting can send it back to Review. Knowledge work isn't
  strictly linear, and a state system that can't represent "this needs
  another pass" will just get ignored.
- **`needs_revision` is a trapdoor, not a stage.** It exists so a problem
  found at any point can be flagged without losing the note's place in the
  pipeline — it always returns to wherever it came from.
- **`--force` exists deliberately.** Rules are useful defaults, not a cage;
  sometimes you really do want to fast-track or bulk-correct a note.

## License

MIT
