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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69adddc52e57d700e447f7090e300ff58cf74c3ca9881eb54c19b488f39222d7
4
- data.tar.gz: 5efcc62978f6d2aeb3689649298956be52ab3fba0bef0ac3ae55603c2afc584f
3
+ metadata.gz: 3745eb09dd23fb800b9cccac6561f493e68e732fc03a3a58c4c2b5e30ccc424c
4
+ data.tar.gz: c8d4a8b7b14f29d9217e28102480d5304e90d7199a1127c577837ff253306150
5
5
  SHA512:
6
- metadata.gz: 202a6ab1a3974e6a303d6571500c230a968114e85003db38c9206908701a51472566f01523637ee032507a1d060df5c2b1272dec001d0a4b76275d64c374628a
7
- data.tar.gz: 9c33747da78d15551c351a884c81ff3a1ef1284079fc6dd627d0284ec6fada9f5ba3f4ec3586f06bf6a56ac958e87a63b066d0a303b7445d60a94f8572b88487
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 `untitled`
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 `untitled` if the result is empty
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
- "empty namespace (its name has no letters/digits; add an explicit 'namespace:')"
147
+ "no usable namespace (its name has no letters/digits; add an explicit 'namespace:')"
144
148
  return nil
145
149
  end
146
150
 
@@ -1,3 +1,3 @@
1
1
  module BaconTracker
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
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.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/\A-|-\z/, '')
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 = filename_slug(title) # never "<id>-.md", which is not a record
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 = filename_slug(title)
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}-#{filename_slug(title)}#{ext}") : path
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bacon-tracker
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Berger