bacon-tracker 1.0.0 → 1.1.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/lib/bacon_tracker/commands/tracker.md +3 -3
- data/lib/bacon_tracker/dashboard.rb +6 -2
- data/lib/bacon_tracker/version.rb +1 -1
- data/lib/bacon_tracker.rb +38 -16
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3745eb09dd23fb800b9cccac6561f493e68e732fc03a3a58c4c2b5e30ccc424c
|
|
4
|
+
data.tar.gz: c8d4a8b7b14f29d9217e28102480d5304e90d7199a1127c577837ff253306150
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a534b8159e6d1498bf9a1cceb51198714bbdc426448d1df087e9b2b51727e723d6874eba65cd9f2175c7e7b513a01ebb5b4144edcddcc08b68e626c076da0f3
|
|
7
|
+
data.tar.gz: a2f9f2594a12b8fbaaf58f25052553fbf9d84075dcd90697960656204f7580918fa717edd96df8fc31fba532ecd9e568dd39a3a847c0b5f27101a70f576349cf
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
All notable changes to this project will be documented here.
|
|
4
4
|
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [1.1.0] - 2026-10-01
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- Filename slugs spell out `ä`, `ö`, `ü`, `ß` as `ae`, `oe`, `ue`, `ss` and drop accents from other letters, so "Datenschutzerklärung" becomes `datenschutzerklaerung` instead of `datenschutzerkl-rung`. Existing files keep their names until the story is retitled.
|
|
11
|
+
- A title with nothing the slug rule keeps (all-CJK, all-punctuation) now slugs to `non-latin-REPLACE-ME` instead of `untitled`, for story files, decision records and dashboard project slugs alike. The dashboard used to get an empty project slug in that case.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Story titles on the board and in the API now come from the in-file `Title:`/`Feature:` line, so umlauts, dots and capitals survive. They were humanized from the filename slug, which keeps only ASCII letters and digits ("Datenschutzerklärung" showed as "datenschutzerkl rung", "bybacon.com" as "bybacon com"). The slug remains the fallback when the line is missing.
|
|
16
|
+
|
|
6
17
|
## [1.0.0] - 2026-09-25
|
|
7
18
|
|
|
8
19
|
First public release.
|
|
@@ -105,7 +105,7 @@ type must be `feature`, `bug`, or `chore`. The title is everything after the typ
|
|
|
105
105
|
1. Determine N: read the integer from `$TRACKER_ROOT/.next-id`, then **floor it above the highest `<NS>-NNN` already on disk** (`N = max(read, highest + 1)`). A stale `.next-id` from a merge or hand edit must never reissue an existing ID. When `rake` is available, prefer `rake story:<type>['title',field=…]`, which allocates the ID atomically (with this floor, under a file lock).
|
|
106
106
|
2. Write `N+1` back to `.next-id`
|
|
107
107
|
3. Pad N to 3 digits: `NNN`
|
|
108
|
-
4. Slugify the title: lowercase it, turn each run of characters that aren't `a-z` or `0-9` into a single hyphen, and trim hyphens from both ends. If nothing is left, the slug is `
|
|
108
|
+
4. Slugify the title: spell out `ä`/`ö`/`ü`/`ß` as `ae`/`oe`/`ue`/`ss`, drop accents from other letters (`é` → `e`), lowercase it, turn each run of characters that aren't `a-z` or `0-9` into a single hyphen, and trim hyphens from both ends. If nothing is left, the slug is `non-latin-REPLACE-ME`
|
|
109
109
|
5. Extension: `.feature` for features, `.md` for bugs and chores
|
|
110
110
|
6. Create `$TRACKER_ROOT/<plural-type>/1_icebox/<NS>-<NNN>-<slug>.<ext>` (e.g. `features/1_icebox/...`). The body is the type's `_template.feature` / `_template.md` if one exists, with the title filled in (`Feature: <title>`, or `Title: <title>` on the first line for `.md`). Write frontmatter **in the file's format**: for `.md`, a YAML block `---` / `id: <NS>-<NNN>` / `type: <type>` / `status: icebox` / `---`; for `.feature`, comment-style header lines `# id: <NS>-<NNN>` / `# type: <type>` / `# status: icebox`. (A `.feature` file is parsed by its `# key:` lines **or** a leading YAML `---` block if one is present - but a file mixes only one style, so match the sibling files in the same tracker rather than introducing the other.)
|
|
111
111
|
7. If any `field=value` tokens were given, set them in the frontmatter (same rules as `edit`)
|
|
@@ -183,7 +183,7 @@ Display a story's content:
|
|
|
183
183
|
|
|
184
184
|
1. `find $TRACKER_ROOT -name "${ID}-*"` to locate the file
|
|
185
185
|
2. Read it, strip the frontmatter block
|
|
186
|
-
3. Print: ID, type, stage, title (humanised from filename), any set metadata fields, then body
|
|
186
|
+
3. Print: ID, type, stage, title (the `Title:`/`Feature:` line; humanised from the filename if that line is missing), any set metadata fields, then body
|
|
187
187
|
|
|
188
188
|
```
|
|
189
189
|
APP-003 - implement user auth
|
|
@@ -264,6 +264,6 @@ Report each finding with the ID and file path, or confirm all checks pass. Don't
|
|
|
264
264
|
|
|
265
265
|
- Expand `~` in any paths using shell: `eval echo <path>` or `realpath`.
|
|
266
266
|
- When editing frontmatter, match the existing format (comment-style for `.feature`, YAML for `.md`).
|
|
267
|
-
- Slugify: `echo "$TITLE" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'` - and use `
|
|
267
|
+
- Slugify: `echo "$TITLE" | sed 's/ä/ae/g; s/ö/oe/g; s/ü/ue/g; s/ß/ss/g; s/Ä/Ae/g; s/Ö/Oe/g; s/Ü/Ue/g' | perl -CS -MUnicode::Normalize -pe '$_ = NFKD($_); s/\p{Mn}//g' | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'` - and use `non-latin-REPLACE-ME` if the result is empty
|
|
268
268
|
- `backlog.md` line format: `- <NS>-NNN <title>` (leading dash, single space - exactly what the gem writes; the stats and next-task parsers depend on it). Preserve this when adding/removing entries.
|
|
269
269
|
- Keep output concise - one line per story, full content only for `show`.
|
|
@@ -136,11 +136,15 @@ module BaconTracker
|
|
|
136
136
|
return nil
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
+
# A name with nothing the slug rule keeps gets the EMPTY_SLUG placeholder
|
|
140
|
+
# as its route (never an empty "/projects/"), but a namespace must not be
|
|
141
|
+
# guessed from it: stories are matched by "<NS>-NNN", so an explicit
|
|
142
|
+
# 'namespace:' is required.
|
|
139
143
|
slug = BaconTracker.slugify(item[:name])
|
|
140
|
-
namespace = item[:attrs]['namespace'] || slug.upcase.gsub('-', '_')
|
|
144
|
+
namespace = item[:attrs]['namespace'] || (slug == BaconTracker::EMPTY_SLUG ? '' : slug.upcase.gsub('-', '_'))
|
|
141
145
|
if namespace.empty?
|
|
142
146
|
warn "[BaconTracker] dashboard.md: skipping project #{item[:name].inspect} - " \
|
|
143
|
-
"
|
|
147
|
+
"no usable namespace (its name has no letters/digits; add an explicit 'namespace:')"
|
|
144
148
|
return nil
|
|
145
149
|
end
|
|
146
150
|
|
data/lib/bacon_tracker.rb
CHANGED
|
@@ -170,11 +170,32 @@ module BaconTracker
|
|
|
170
170
|
@config || configure
|
|
171
171
|
end
|
|
172
172
|
|
|
173
|
+
# German letters have a spelled-out ASCII form that readers expect in a
|
|
174
|
+
# filename ("datenschutzerklaerung", not "datenschutzerkl-rung"); every
|
|
175
|
+
# other accented letter loses its mark via NFKD decomposition (é -> e).
|
|
176
|
+
TRANSLITERATIONS = {
|
|
177
|
+
'ä' => 'ae', 'ö' => 'oe', 'ü' => 'ue', 'ß' => 'ss',
|
|
178
|
+
'Ä' => 'Ae', 'Ö' => 'Oe', 'Ü' => 'Ue', 'ẞ' => 'SS'
|
|
179
|
+
}.freeze
|
|
180
|
+
|
|
181
|
+
# What slugify returns when nothing of the title survives the ASCII rule
|
|
182
|
+
# (all-CJK, all-punctuation). Never "": an empty slug makes a dangling
|
|
183
|
+
# "<id>-.md" that is not a record (BT-095) and a dashboard route of
|
|
184
|
+
# "/projects/". The placeholder says what happened and asks for a hand-picked
|
|
185
|
+
# slug; the real title stays in the file's Title:/Feature: line.
|
|
186
|
+
EMPTY_SLUG = 'non-latin-REPLACE-ME'
|
|
187
|
+
|
|
173
188
|
# Filesystem-safe slug from a human title - the single source shared by
|
|
174
189
|
# Core (story filenames) and Dashboard (project slugs) so the two can never
|
|
175
|
-
# drift apart.
|
|
190
|
+
# drift apart. ASCII-only: the German letters are transliterated, other
|
|
191
|
+
# accents stripped, and whatever is still not [a-z0-9] becomes a hyphen.
|
|
192
|
+
# Never empty - see EMPTY_SLUG.
|
|
176
193
|
def self.slugify(title)
|
|
177
|
-
title.
|
|
194
|
+
slug = title.to_s
|
|
195
|
+
.gsub(/[äöüßÄÖÜẞ]/, TRANSLITERATIONS)
|
|
196
|
+
.unicode_normalize(:nfkd).gsub(/\p{Mn}/, '')
|
|
197
|
+
.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/\A-|-\z/, '')
|
|
198
|
+
slug.empty? ? EMPTY_SLUG : slug
|
|
178
199
|
end
|
|
179
200
|
|
|
180
201
|
class Core
|
|
@@ -313,16 +334,6 @@ module BaconTracker
|
|
|
313
334
|
BaconTracker.slugify(title)
|
|
314
335
|
end
|
|
315
336
|
|
|
316
|
-
# A non-empty slug for a filename. slugify keeps only [a-z0-9], so a title
|
|
317
|
-
# with no ASCII alphanumerics (all-CJK, all-punctuation) slugs to "" - fall
|
|
318
|
-
# back to 'untitled' so the file is "<id>-untitled.ext", not a dangling
|
|
319
|
-
# "<id>-.ext" with a blank humanized name (BT-095). The real title stays in
|
|
320
|
-
# the file's Title:/Feature: line.
|
|
321
|
-
def filename_slug(title)
|
|
322
|
-
slug = slugify(title)
|
|
323
|
-
slug.empty? ? 'untitled' : slug
|
|
324
|
-
end
|
|
325
|
-
|
|
326
337
|
def story_dirs
|
|
327
338
|
STORY_DIRS.flat_map do |dir, meta|
|
|
328
339
|
STAGES.map do |stage|
|
|
@@ -404,6 +415,17 @@ module BaconTracker
|
|
|
404
415
|
base.sub(/\A#{id_pattern}-/, '').gsub('-', ' ')
|
|
405
416
|
end
|
|
406
417
|
|
|
418
|
+
# The in-file title: the "Feature:" line of a .feature, the "Title:" line
|
|
419
|
+
# of a .md. The filename slug is ASCII-only, so a humanized slug loses
|
|
420
|
+
# umlauts, dots and case ("Datenschutzerklärung" shows as
|
|
421
|
+
# "datenschutzerklaerung"); the in-file line is the title as typed. nil when
|
|
422
|
+
# the line is missing or blank, so callers fall back to the slug.
|
|
423
|
+
def story_title(body, ext)
|
|
424
|
+
line = ext == '.feature' ? /^Feature:[ \t]*(.*)$/ : /^Title:[ \t]*(.*)$/
|
|
425
|
+
title = body[line, 1]&.strip
|
|
426
|
+
title unless title.nil? || title.empty?
|
|
427
|
+
end
|
|
428
|
+
|
|
407
429
|
# Delegates to set_frontmatter_field: the substitution stays scoped to the
|
|
408
430
|
# frontmatter block (a body line like "status: healthy" is never touched)
|
|
409
431
|
# and a missing status field is added instead of silently skipped.
|
|
@@ -1324,7 +1346,7 @@ module BaconTracker
|
|
|
1324
1346
|
with_decisions_lock do
|
|
1325
1347
|
heal_proposed!
|
|
1326
1348
|
id = format_decision_id(consume_decision_id)
|
|
1327
|
-
slug =
|
|
1349
|
+
slug = slugify(title) # never "<id>-.md", which is not a record (EMPTY_SLUG)
|
|
1328
1350
|
template_path = File.join(root, '_template.md')
|
|
1329
1351
|
body = File.exist?(template_path) ? File.read(template_path, encoding: 'utf-8') : Templates::DECISION
|
|
1330
1352
|
today = Date.today.iso8601
|
|
@@ -1660,7 +1682,7 @@ module BaconTracker
|
|
|
1660
1682
|
subtasks = subtask_counts(body, ext)
|
|
1661
1683
|
|
|
1662
1684
|
{ id: id, type: dir_meta[:type], stage: dir_meta[:stage], dir: dir_meta[:dir],
|
|
1663
|
-
path: path, title: humanize_slug(path), body: body, size: size,
|
|
1685
|
+
path: path, title: story_title(body, ext) || humanize_slug(path), body: body, size: size,
|
|
1664
1686
|
blocked_by: blocked_by, linked_to: linked_to, assignee: assignee, subtasks: subtasks,
|
|
1665
1687
|
# Body-line addresses of the subtasks - the client renders/toggles by
|
|
1666
1688
|
# these instead of re-deriving fence rules (the BT-049 drift class).
|
|
@@ -1851,7 +1873,7 @@ module BaconTracker
|
|
|
1851
1873
|
with_lock do
|
|
1852
1874
|
id_num = consume_id
|
|
1853
1875
|
id = format_id(id_num)
|
|
1854
|
-
slug =
|
|
1876
|
+
slug = slugify(title)
|
|
1855
1877
|
|
|
1856
1878
|
filename = "#{id}-#{slug}#{ext}"
|
|
1857
1879
|
dest_dir = File.join(@config.tracker_root, dir, stage)
|
|
@@ -1975,7 +1997,7 @@ module BaconTracker
|
|
|
1975
1997
|
|
|
1976
1998
|
if title || body || !size.nil? || !blocked_by.nil? || !linked_to.nil? || !assignee.nil?
|
|
1977
1999
|
atomic_write(path, content)
|
|
1978
|
-
new_path = title ? File.join(File.dirname(path), "#{id}-#{
|
|
2000
|
+
new_path = title ? File.join(File.dirname(path), "#{id}-#{slugify(title)}#{ext}") : path
|
|
1979
2001
|
if path != new_path
|
|
1980
2002
|
FileUtils.mv(path, new_path)
|
|
1981
2003
|
path = new_path
|