note-state 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- note_state/__init__.py +3 -0
- note_state/cli.py +251 -0
- note_state/default_states.yaml +62 -0
- note_state-0.1.0.dist-info/METADATA +177 -0
- note_state-0.1.0.dist-info/RECORD +9 -0
- note_state-0.1.0.dist-info/WHEEL +5 -0
- note_state-0.1.0.dist-info/entry_points.txt +2 -0
- note_state-0.1.0.dist-info/licenses/LICENSE +21 -0
- note_state-0.1.0.dist-info/top_level.txt +1 -0
note_state/__init__.py
ADDED
note_state/cli.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""
|
|
2
|
+
note_state.cli — a portable state-tracking layer for markdown-based note apps.
|
|
3
|
+
|
|
4
|
+
Works with any note application that stores notes as markdown files with
|
|
5
|
+
YAML frontmatter on local disk: Obsidian, Dendron, Foam, plain markdown
|
|
6
|
+
vaults, and similar. (See the project README for notes on Logseq/Notion,
|
|
7
|
+
which store notes a little differently.)
|
|
8
|
+
|
|
9
|
+
It does one job: read/write a `state` field (plus a `state_history` log)
|
|
10
|
+
in each note's frontmatter, and enforce the transition rules defined in
|
|
11
|
+
states.yaml so a note can't silently skip stages of the pipeline.
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
note-state init <file_or_dir>
|
|
15
|
+
note-state set <file> <state> [--force] [--note "reason"]
|
|
16
|
+
note-state list [dir] [--state STATE]
|
|
17
|
+
note-state report [dir]
|
|
18
|
+
note-state graph
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import datetime
|
|
23
|
+
import importlib.resources
|
|
24
|
+
import os
|
|
25
|
+
import pathlib
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
import yaml
|
|
29
|
+
|
|
30
|
+
from . import __version__
|
|
31
|
+
|
|
32
|
+
DEFAULT_CONFIG_NAMES = ["states.yaml", ".note-states.yaml"]
|
|
33
|
+
FRONTMATTER_DELIM = "---"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------- config --
|
|
37
|
+
|
|
38
|
+
def find_local_config():
|
|
39
|
+
"""Look for states.yaml in the current directory or any parent."""
|
|
40
|
+
d = pathlib.Path.cwd()
|
|
41
|
+
while True:
|
|
42
|
+
for name in DEFAULT_CONFIG_NAMES:
|
|
43
|
+
p = d / name
|
|
44
|
+
if p.exists():
|
|
45
|
+
return str(p)
|
|
46
|
+
if d.parent == d:
|
|
47
|
+
return None
|
|
48
|
+
d = d.parent
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def load_config(path=None):
|
|
52
|
+
if path:
|
|
53
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
54
|
+
cfg = yaml.safe_load(f)
|
|
55
|
+
else:
|
|
56
|
+
local = find_local_config()
|
|
57
|
+
if local:
|
|
58
|
+
with open(local, "r", encoding="utf-8") as f:
|
|
59
|
+
cfg = yaml.safe_load(f)
|
|
60
|
+
else:
|
|
61
|
+
# Fall back to the bundled default pipeline.
|
|
62
|
+
data = importlib.resources.files("note_state").joinpath("default_states.yaml")
|
|
63
|
+
cfg = yaml.safe_load(data.read_text(encoding="utf-8"))
|
|
64
|
+
|
|
65
|
+
if "states" not in cfg or "transitions" not in cfg or "initial_state" not in cfg:
|
|
66
|
+
sys.exit("Config is missing required keys: states, transitions, initial_state")
|
|
67
|
+
return cfg
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------- frontmatter --
|
|
71
|
+
|
|
72
|
+
def read_note(path):
|
|
73
|
+
"""Return (meta_dict, body_text) for a markdown file, tolerating no frontmatter."""
|
|
74
|
+
text = pathlib.Path(path).read_text(encoding="utf-8")
|
|
75
|
+
if text.startswith(FRONTMATTER_DELIM):
|
|
76
|
+
parts = text.split(FRONTMATTER_DELIM, 2)
|
|
77
|
+
if len(parts) == 3:
|
|
78
|
+
meta = yaml.safe_load(parts[1]) or {}
|
|
79
|
+
body = parts[2].lstrip("\n")
|
|
80
|
+
return meta, body
|
|
81
|
+
return {}, text
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def write_note(path, meta, body):
|
|
85
|
+
yaml_str = yaml.safe_dump(
|
|
86
|
+
meta, sort_keys=False, allow_unicode=True, default_flow_style=False
|
|
87
|
+
)
|
|
88
|
+
content = f"{FRONTMATTER_DELIM}\n{yaml_str}{FRONTMATTER_DELIM}\n\n{body}"
|
|
89
|
+
pathlib.Path(path).write_text(content, encoding="utf-8")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def iter_md_files(root):
|
|
93
|
+
if os.path.isfile(root):
|
|
94
|
+
yield root
|
|
95
|
+
return
|
|
96
|
+
for dirpath, _, filenames in os.walk(root):
|
|
97
|
+
for fn in filenames:
|
|
98
|
+
if fn.endswith(".md"):
|
|
99
|
+
yield os.path.join(dirpath, fn)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def today_iso():
|
|
103
|
+
return datetime.date.today().isoformat()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# -------------------------------------------------------------- commands --
|
|
107
|
+
|
|
108
|
+
def cmd_init(args, cfg):
|
|
109
|
+
initial = cfg["initial_state"]
|
|
110
|
+
touched = 0
|
|
111
|
+
for path in iter_md_files(args.target):
|
|
112
|
+
meta, body = read_note(path)
|
|
113
|
+
if "state" in meta:
|
|
114
|
+
print(f"skip {path} (already: {meta['state']})")
|
|
115
|
+
continue
|
|
116
|
+
meta["state"] = initial
|
|
117
|
+
meta["state_history"] = [{"state": initial, "date": today_iso()}]
|
|
118
|
+
write_note(path, meta, body)
|
|
119
|
+
print(f"init {path} -> {initial}")
|
|
120
|
+
touched += 1
|
|
121
|
+
print(f"\n{touched} note(s) initialized.")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def cmd_set(args, cfg):
|
|
125
|
+
path = args.file
|
|
126
|
+
meta, body = read_note(path)
|
|
127
|
+
current = meta.get("state", cfg["initial_state"])
|
|
128
|
+
target = args.state
|
|
129
|
+
|
|
130
|
+
if target not in cfg["states"]:
|
|
131
|
+
valid = ", ".join(cfg["states"].keys())
|
|
132
|
+
sys.exit(f"Unknown state '{target}'. Valid states: {valid}")
|
|
133
|
+
|
|
134
|
+
allowed = cfg["transitions"].get(current, [])
|
|
135
|
+
if target not in allowed and not args.force:
|
|
136
|
+
allowed_str = ", ".join(allowed) if allowed else "(none — terminal state)"
|
|
137
|
+
sys.exit(
|
|
138
|
+
f"Can't move '{path}' from '{current}' to '{target}'.\n"
|
|
139
|
+
f"Allowed next states from '{current}': {allowed_str}\n"
|
|
140
|
+
f"Use --force to override anyway."
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
meta["state"] = target
|
|
144
|
+
history = meta.setdefault("state_history", [])
|
|
145
|
+
entry = {"state": target, "date": today_iso()}
|
|
146
|
+
if args.note:
|
|
147
|
+
entry["note"] = args.note
|
|
148
|
+
history.append(entry)
|
|
149
|
+
write_note(path, meta, body)
|
|
150
|
+
print(f"{path}: {current} -> {target}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def cmd_list(args, cfg):
|
|
154
|
+
rows = []
|
|
155
|
+
for path in iter_md_files(args.target):
|
|
156
|
+
meta, _ = read_note(path)
|
|
157
|
+
state = meta.get("state", "(none)")
|
|
158
|
+
if args.state and state != args.state:
|
|
159
|
+
continue
|
|
160
|
+
history = meta.get("state_history", [])
|
|
161
|
+
last_date = history[-1]["date"] if history else "?"
|
|
162
|
+
rows.append((state, last_date, path))
|
|
163
|
+
|
|
164
|
+
if not rows:
|
|
165
|
+
print("No notes found.")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
rows.sort(key=lambda r: (r[0], r[2]))
|
|
169
|
+
width = max(len(r[0]) for r in rows)
|
|
170
|
+
for state, last_date, path in rows:
|
|
171
|
+
print(f"{state.ljust(width)} {last_date} {path}")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def cmd_report(args, cfg):
|
|
175
|
+
counts = {}
|
|
176
|
+
aged = []
|
|
177
|
+
today = datetime.date.today()
|
|
178
|
+
|
|
179
|
+
for path in iter_md_files(args.target):
|
|
180
|
+
meta, _ = read_note(path)
|
|
181
|
+
state = meta.get("state", "(none)")
|
|
182
|
+
counts[state] = counts.get(state, 0) + 1
|
|
183
|
+
history = meta.get("state_history", [])
|
|
184
|
+
if history:
|
|
185
|
+
try:
|
|
186
|
+
last = datetime.date.fromisoformat(history[-1]["date"])
|
|
187
|
+
aged.append(((today - last).days, state, path))
|
|
188
|
+
except ValueError:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
print("Notes per state:")
|
|
192
|
+
for state, n in sorted(counts.items(), key=lambda kv: -kv[1]):
|
|
193
|
+
print(f" {state.ljust(16)} {n}")
|
|
194
|
+
|
|
195
|
+
if aged:
|
|
196
|
+
aged.sort(reverse=True)
|
|
197
|
+
print("\nOldest since last transition (possible bottlenecks):")
|
|
198
|
+
for age, state, path in aged[:10]:
|
|
199
|
+
print(f" {age:>4}d {state.ljust(16)} {path}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def cmd_graph(args, cfg):
|
|
203
|
+
print("stateDiagram-v2")
|
|
204
|
+
for src, dests in cfg["transitions"].items():
|
|
205
|
+
if not dests:
|
|
206
|
+
continue
|
|
207
|
+
for dest in dests:
|
|
208
|
+
print(f" {src} --> {dest}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ------------------------------------------------------------------ main --
|
|
212
|
+
|
|
213
|
+
def main():
|
|
214
|
+
parser = argparse.ArgumentParser(
|
|
215
|
+
prog="note-state",
|
|
216
|
+
description="Track note states through a knowledge-processing pipeline.",
|
|
217
|
+
)
|
|
218
|
+
parser.add_argument("--version", action="version", version=f"note-state {__version__}")
|
|
219
|
+
parser.add_argument("--config", help="path to states.yaml", default=None)
|
|
220
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
221
|
+
|
|
222
|
+
p_init = sub.add_parser("init", help="stamp an initial state onto notes that don't have one")
|
|
223
|
+
p_init.add_argument("target", help="file or directory")
|
|
224
|
+
p_init.set_defaults(func=cmd_init)
|
|
225
|
+
|
|
226
|
+
p_set = sub.add_parser("set", help="transition a note to a new state")
|
|
227
|
+
p_set.add_argument("file")
|
|
228
|
+
p_set.add_argument("state")
|
|
229
|
+
p_set.add_argument("--force", action="store_true", help="allow a transition not listed in states.yaml")
|
|
230
|
+
p_set.add_argument("--note", default=None, help="optional short reason, stored in state_history")
|
|
231
|
+
p_set.set_defaults(func=cmd_set)
|
|
232
|
+
|
|
233
|
+
p_list = sub.add_parser("list", help="list notes and their current state")
|
|
234
|
+
p_list.add_argument("target", nargs="?", default=".")
|
|
235
|
+
p_list.add_argument("--state", default=None, help="filter to a single state")
|
|
236
|
+
p_list.set_defaults(func=cmd_list)
|
|
237
|
+
|
|
238
|
+
p_report = sub.add_parser("report", help="counts per state + oldest unmoved notes")
|
|
239
|
+
p_report.add_argument("target", nargs="?", default=".")
|
|
240
|
+
p_report.set_defaults(func=cmd_report)
|
|
241
|
+
|
|
242
|
+
p_graph = sub.add_parser("graph", help="print the state machine as a Mermaid diagram")
|
|
243
|
+
p_graph.set_defaults(func=cmd_graph)
|
|
244
|
+
|
|
245
|
+
args = parser.parse_args()
|
|
246
|
+
cfg = load_config(args.config)
|
|
247
|
+
args.func(args, cfg)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
main()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Bundled default state machine — used automatically when no states.yaml
|
|
2
|
+
# is found in the current directory or any parent. Copy this file into
|
|
3
|
+
# your vault as `states.yaml` (or `.note-states.yaml`) and edit freely;
|
|
4
|
+
# a local copy always takes priority over this bundled default.
|
|
5
|
+
|
|
6
|
+
initial_state: drafting
|
|
7
|
+
|
|
8
|
+
states:
|
|
9
|
+
drafting:
|
|
10
|
+
label: "Drafting"
|
|
11
|
+
description: >
|
|
12
|
+
Raw capture. Get the idea down with as little friction as possible —
|
|
13
|
+
fragments, bullet points, half-formed sentences are all fine here.
|
|
14
|
+
review:
|
|
15
|
+
label: "Review"
|
|
16
|
+
description: >
|
|
17
|
+
Check the idea for accuracy, internal consistency, and missing gaps.
|
|
18
|
+
Decide whether it deserves to keep moving or needs rework.
|
|
19
|
+
formatting:
|
|
20
|
+
label: "Formatting"
|
|
21
|
+
description: >
|
|
22
|
+
Give the note a shape: headings, atomic scope (one idea per note),
|
|
23
|
+
consistent title, clean prose, tags.
|
|
24
|
+
referencing:
|
|
25
|
+
label: "Referencing"
|
|
26
|
+
description: >
|
|
27
|
+
Attach provenance: citations, source links, backlinks to the notes
|
|
28
|
+
this one draws on or was extracted from.
|
|
29
|
+
integrating:
|
|
30
|
+
label: "Integrating"
|
|
31
|
+
description: >
|
|
32
|
+
Weave the note into the graph: link it from relevant Maps of
|
|
33
|
+
Content / index notes, cross-link related permanent notes, make it
|
|
34
|
+
discoverable through search and tags rather than sitting orphaned.
|
|
35
|
+
published:
|
|
36
|
+
label: "Published"
|
|
37
|
+
description: >
|
|
38
|
+
Stable and reusable. Safe to cite, export, or share outside the
|
|
39
|
+
vault. The "done" state of the main pipeline.
|
|
40
|
+
needs_revision:
|
|
41
|
+
label: "Needs Revision"
|
|
42
|
+
description: >
|
|
43
|
+
Flagged during any stage as having a problem (factual error,
|
|
44
|
+
unclear scope, broken links). Always exits back to the stage that
|
|
45
|
+
flagged it, once fixed.
|
|
46
|
+
archived:
|
|
47
|
+
label: "Archived"
|
|
48
|
+
description: >
|
|
49
|
+
Retired or superseded by another note. No longer part of the
|
|
50
|
+
active pipeline, but kept for the record.
|
|
51
|
+
|
|
52
|
+
# Map of state -> list of states it's allowed to move to.
|
|
53
|
+
# cmd `set` enforces this unless you pass --force.
|
|
54
|
+
transitions:
|
|
55
|
+
drafting: [review, archived]
|
|
56
|
+
review: [formatting, drafting, needs_revision, archived]
|
|
57
|
+
formatting: [referencing, review, needs_revision, archived]
|
|
58
|
+
referencing: [integrating, formatting, needs_revision, archived]
|
|
59
|
+
integrating: [published, referencing, needs_revision, archived]
|
|
60
|
+
published: [integrating, archived]
|
|
61
|
+
needs_revision: [drafting, review, formatting, referencing, integrating]
|
|
62
|
+
archived: []
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: note-state
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A portable state-tracking layer for markdown note vaults (Obsidian, Dendron, Foam, and plain markdown folders).
|
|
5
|
+
Author-email: superxgen <superxgen@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/resystem0/note-state
|
|
8
|
+
Project-URL: Issues, https://github.com/resystem0/note-state/issues
|
|
9
|
+
Keywords: notes,markdown,obsidian,pkm,zettelkasten,frontmatter,state-machine
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: PyYAML>=6.0
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# note-state
|
|
25
|
+
|
|
26
|
+
A portable "patch" for turning a pile of notes into a *pipeline*: every note
|
|
27
|
+
carries an explicit `state` telling you how far it's traveled from raw
|
|
28
|
+
capture to integrated, trustworthy knowledge — the same way a ticket in an
|
|
29
|
+
issue tracker carries a status, or a part on an assembly line carries a
|
|
30
|
+
stage.
|
|
31
|
+
|
|
32
|
+
Works with any note application that stores notes as markdown files with
|
|
33
|
+
**YAML frontmatter** on local disk — Obsidian, Dendron, Foam, Logseq, plain
|
|
34
|
+
markdown vaults, even a folder of `.md` files with no app at all.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install note-state
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## The pipeline
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
drafting → review → formatting → referencing → integrating → published
|
|
46
|
+
↘ ↘ ↘ ↘
|
|
47
|
+
needs_revision (loops back to whichever
|
|
48
|
+
stage flagged the problem)
|
|
49
|
+
|
|
50
|
+
any state ──────────────────────────────→ archived
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
| State | What happens here | Exit condition |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| **Drafting** | Raw capture. Get the idea down with as little friction as possible. | The idea has enough substance to be judged. |
|
|
56
|
+
| **Review** | Read it critically: accurate, internally consistent, complete? | Content is verified, or it's sent back to Drafting. |
|
|
57
|
+
| **Formatting** | Give it shape: headings, atomic scope, a real title, clean prose, tags. | It reads well and is scoped correctly. |
|
|
58
|
+
| **Referencing** | Attach provenance: citations, source links, backlinks. | Every non-obvious claim is traceable to a source. |
|
|
59
|
+
| **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. |
|
|
60
|
+
| **Published** | Stable and reusable — safe to cite, export, or share. | Terminal, but can be pulled back into Integrating. |
|
|
61
|
+
| **Needs Revision** *(utility state)* | Something's wrong. Flagged from any stage. | Always exits back to the stage that flagged it. |
|
|
62
|
+
| **Archived** *(utility state)* | Retired or superseded. | Terminal. |
|
|
63
|
+
|
|
64
|
+
This isn't sacred — it's just the bundled default. Drop your own
|
|
65
|
+
`states.yaml` next to your notes (or in any parent directory) to define a
|
|
66
|
+
different pipeline; `note-state` doesn't hardcode the states, it just
|
|
67
|
+
enforces whatever pipeline you describe.
|
|
68
|
+
|
|
69
|
+
## The metadata schema
|
|
70
|
+
|
|
71
|
+
Every tracked note gets two frontmatter fields: the current state, and a
|
|
72
|
+
timestamped log of how it got there.
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
---
|
|
76
|
+
title: "Maps of Content connect clusters of related notes"
|
|
77
|
+
state: referencing
|
|
78
|
+
state_history:
|
|
79
|
+
- state: drafting
|
|
80
|
+
date: 2026-07-01
|
|
81
|
+
- state: review
|
|
82
|
+
date: 2026-07-05
|
|
83
|
+
- state: formatting
|
|
84
|
+
date: 2026-07-08
|
|
85
|
+
- state: referencing
|
|
86
|
+
date: 2026-07-12
|
|
87
|
+
note: "added three sources"
|
|
88
|
+
tags: [pkm, moc]
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
Note body goes here as normal — nothing about the state system touches
|
|
92
|
+
the content itself.
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`state_history` is what makes the system useful beyond a label: it's an
|
|
96
|
+
audit trail you can query later ("which notes have been stuck in Review for
|
|
97
|
+
three weeks?", "how long does formatting usually take?").
|
|
98
|
+
|
|
99
|
+
## Usage
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Stamp every note in a folder with the initial state (only touches
|
|
103
|
+
# notes that don't already have one — safe to run repeatedly)
|
|
104
|
+
note-state init ./my-vault
|
|
105
|
+
|
|
106
|
+
# Move a note forward
|
|
107
|
+
note-state set my-vault/atomic-notes.md review
|
|
108
|
+
|
|
109
|
+
# Try to skip a stage — this is rejected unless you pass --force
|
|
110
|
+
note-state set my-vault/atomic-notes.md published
|
|
111
|
+
|
|
112
|
+
# See what's where
|
|
113
|
+
note-state list ./my-vault
|
|
114
|
+
note-state list ./my-vault --state review
|
|
115
|
+
|
|
116
|
+
# Find bottlenecks — counts per state + oldest untouched notes
|
|
117
|
+
note-state report ./my-vault
|
|
118
|
+
|
|
119
|
+
# Print the pipeline as a Mermaid diagram (paste into anything that renders it)
|
|
120
|
+
note-state graph
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Customizing the pipeline
|
|
124
|
+
|
|
125
|
+
Two files define the state machine:
|
|
126
|
+
|
|
127
|
+
- **`states.yaml`** — states + allowed transitions. `note-state` looks for
|
|
128
|
+
this file (or `.note-states.yaml`) in the current directory, then any
|
|
129
|
+
parent directory, and falls back to the bundled default pipeline above if
|
|
130
|
+
none is found. Copy the bundled default as a starting point and edit
|
|
131
|
+
freely — rename stages, add a `translating` or `peer-review` state,
|
|
132
|
+
rewire transitions.
|
|
133
|
+
- Pass `--config path/to/states.yaml` to point at a specific file instead
|
|
134
|
+
of relying on directory discovery.
|
|
135
|
+
|
|
136
|
+
## Wiring it into a specific app
|
|
137
|
+
|
|
138
|
+
- **Obsidian, Dendron, Foam, plain markdown folders** — these already read
|
|
139
|
+
and preserve YAML frontmatter, so `note-state` works with zero
|
|
140
|
+
adaptation: run it straight against your vault folder. In Obsidian, the
|
|
141
|
+
[Dataview](https://github.com/blacksmithgu/obsidian-dataview) plugin can
|
|
142
|
+
turn `state` into a live board, e.g.:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
TABLE state, state_history[-1].date AS "last moved"
|
|
146
|
+
FROM "Notes"
|
|
147
|
+
SORT state
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- **Logseq** — pages are markdown files too, but Logseq's own metadata
|
|
151
|
+
lives as a `key:: value` line on the first block rather than a `---`
|
|
152
|
+
frontmatter block. Keep frontmatter *and* Logseq properties side by side
|
|
153
|
+
(Logseq ignores the frontmatter block, `note-state` ignores the `::`
|
|
154
|
+
line), or adapt the parser in `note_state/cli.py` to read
|
|
155
|
+
`state:: drafting` instead.
|
|
156
|
+
|
|
157
|
+
- **Notion** (or any database-backed app without local files) — recreate
|
|
158
|
+
`states.yaml` as a **Status** property with matching options, and use a
|
|
159
|
+
Board view grouped by it. You lose `note-state`'s enforcement of
|
|
160
|
+
transition rules unless you script it against the Notion API, but the
|
|
161
|
+
state vocabulary and pipeline shape carry over directly.
|
|
162
|
+
|
|
163
|
+
## Design notes
|
|
164
|
+
|
|
165
|
+
- **Backward moves are allowed on purpose.** Review can send a note back to
|
|
166
|
+
Drafting; Formatting can send it back to Review. Knowledge work isn't
|
|
167
|
+
strictly linear, and a state system that can't represent "this needs
|
|
168
|
+
another pass" will just get ignored.
|
|
169
|
+
- **`needs_revision` is a trapdoor, not a stage.** It exists so a problem
|
|
170
|
+
found at any point can be flagged without losing the note's place in the
|
|
171
|
+
pipeline — it always returns to wherever it came from.
|
|
172
|
+
- **`--force` exists deliberately.** Rules are useful defaults, not a cage;
|
|
173
|
+
sometimes you really do want to fast-track or bulk-correct a note.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
note_state/__init__.py,sha256=EfgXMRAmWn2fjHw_V1DnoVMAnPKo6-Zm-Zfcamsx3Vw,102
|
|
2
|
+
note_state/cli.py,sha256=ogm3ao6RmzurMxxcnU3trmVzSRZKcZm5VYK-nntTaPo,8362
|
|
3
|
+
note_state/default_states.yaml,sha256=M755AD8e2OCGZTAKLBxcZytD2f7m-mGewsyZwQckEAk,2489
|
|
4
|
+
note_state-0.1.0.dist-info/licenses/LICENSE,sha256=kly0zl_87o8kF1kD2kW0EJIqFOirFiZPYRw5X3BOXkQ,1066
|
|
5
|
+
note_state-0.1.0.dist-info/METADATA,sha256=ArYtmw15IgoCZH10Tbga02WEOjhnqcp4x-QeEuuxgNI,7137
|
|
6
|
+
note_state-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
7
|
+
note_state-0.1.0.dist-info/entry_points.txt,sha256=p_oMzKEtUQsiXkyhYEB7Nbop3QbQ4JYOYTW1pLUFUIM,51
|
|
8
|
+
note_state-0.1.0.dist-info/top_level.txt,sha256=rN_hy_ZhAEaNiK62LQwFhAOBEsEO_eQZm_tKwoAxzZU,11
|
|
9
|
+
note_state-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 resystem0
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
note_state
|