opencode-skills-collection 4.0.41 → 4.0.43

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.
Files changed (73) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +5 -2
  2. package/bundled-skills/animejs-animation/SKILL.md +1 -1
  3. package/bundled-skills/anti-ui-slop/SKILL.md +51 -86
  4. package/bundled-skills/antigravity-design-expert/SKILL.md +1 -1
  5. package/bundled-skills/design-spells/SKILL.md +1 -1
  6. package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
  7. package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
  8. package/bundled-skills/docs/maintainers/repo-growth-seo.md +1 -1
  9. package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
  10. package/bundled-skills/docs/users/aas-core.md +1 -1
  11. package/bundled-skills/docs/users/bundles.md +1 -1
  12. package/bundled-skills/docs/users/claude-code-skills.md +1 -1
  13. package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
  14. package/bundled-skills/docs/users/kiro-integration.md +1 -1
  15. package/bundled-skills/docs/users/usage.md +3 -3
  16. package/bundled-skills/docs/users/visual-guide.md +4 -4
  17. package/bundled-skills/famulor-skill/SKILL.md +132 -0
  18. package/bundled-skills/famulor-skill/references/assistant-design.md +137 -0
  19. package/bundled-skills/famulor-skill/references/toolsets/assistants.md +64 -0
  20. package/bundled-skills/famulor-skill/references/toolsets/automations.md +36 -0
  21. package/bundled-skills/famulor-skill/references/toolsets/billing.md +15 -0
  22. package/bundled-skills/famulor-skill/references/toolsets/calls.md +23 -0
  23. package/bundled-skills/famulor-skill/references/toolsets/campaigns.md +42 -0
  24. package/bundled-skills/famulor-skill/references/toolsets/dashboards.md +27 -0
  25. package/bundled-skills/famulor-skill/references/toolsets/knowledge.md +28 -0
  26. package/bundled-skills/famulor-skill/references/toolsets/messaging.md +52 -0
  27. package/bundled-skills/famulor-skill/references/toolsets/migration.md +10 -0
  28. package/bundled-skills/famulor-skill/references/toolsets/platform.md +14 -0
  29. package/bundled-skills/famulor-skill/references/toolsets/settings.md +28 -0
  30. package/bundled-skills/famulor-skill/references/toolsets/tasks.md +12 -0
  31. package/bundled-skills/famulor-skill/references/toolsets/telephony.md +35 -0
  32. package/bundled-skills/iconsax-library/SKILL.md +1 -1
  33. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package-lock.json +4 -4
  34. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package.json +1 -1
  35. package/bundled-skills/lore/SKILL.md +104 -298
  36. package/bundled-skills/lore/references/audit-template.md +21 -4
  37. package/bundled-skills/lore/references/compatibility.md +51 -121
  38. package/bundled-skills/lore/references/config.md +22 -23
  39. package/bundled-skills/lore/references/entry-format.md +33 -3
  40. package/bundled-skills/lore/references/history-command.md +98 -2
  41. package/bundled-skills/lore/references/platform-mirrors.md +45 -16
  42. package/bundled-skills/lore/references/stale-new-markers.md +10 -8
  43. package/bundled-skills/lore/references/summary-template.md +8 -1
  44. package/bundled-skills/lore/references/workflows.md +192 -0
  45. package/bundled-skills/lore/scripts/README.md +14 -13
  46. package/bundled-skills/lore/scripts/README.zh-CN.md +14 -13
  47. package/bundled-skills/lore/scripts/find_duplicates.py +14 -4
  48. package/bundled-skills/lore/scripts/find_stale.py +68 -16
  49. package/bundled-skills/lore/scripts/history.py +235 -23
  50. package/bundled-skills/lore/scripts/id_hash.py +7 -4
  51. package/bundled-skills/lore/scripts/list_entries.py +82 -13
  52. package/bundled-skills/magic-animator/SKILL.md +1 -1
  53. package/bundled-skills/magic-ui-generator/SKILL.md +1 -1
  54. package/bundled-skills/poka-yoke/SKILL.md +172 -0
  55. package/bundled-skills/project-state-governor/SKILL.md +483 -0
  56. package/bundled-skills/project-state-governor/references/manifest-routing.md +47 -0
  57. package/bundled-skills/project-state-governor/references/persistence-lifecycle.md +93 -0
  58. package/bundled-skills/project-state-governor/references/project-state-schema.md +173 -0
  59. package/bundled-skills/project-state-governor/references/reconstruction-workflow.md +183 -0
  60. package/bundled-skills/salesforce-development/SKILL.md +107 -37
  61. package/bundled-skills/spec-driven-loop/SKILL.md +203 -0
  62. package/bundled-skills/spec-driven-loop/references/agent-and-judge-contracts.md +120 -0
  63. package/bundled-skills/spec-driven-loop/references/document-templates.md +184 -0
  64. package/bundled-skills/unsplash-integration/SKILL.md +1 -1
  65. package/bundled-skills/vizcom/SKILL.md +1 -1
  66. package/bundled-skills/x-twitter-scraper/SKILL.md +20 -7
  67. package/package.json +1 -1
  68. package/skills_index.json +136 -36
  69. package/bundled-skills/lore/README.md +0 -386
  70. package/bundled-skills/lore/README.zh-CN.md +0 -386
  71. package/bundled-skills/lore/WORKFLOWS.md +0 -216
  72. package/bundled-skills/lore/WORKFLOWS.zh-CN.md +0 -216
  73. package/bundled-skills/uizze-ui-research/SKILL.md +0 -128
@@ -42,6 +42,8 @@ def get_entries():
42
42
  [sys.executable, str(script), "--json"],
43
43
  capture_output=True,
44
44
  text=True,
45
+ encoding="utf-8",
46
+ errors="replace",
45
47
  )
46
48
  if r.returncode != 0:
47
49
  print(r.stderr, file=sys.stderr)
@@ -128,6 +130,11 @@ def hash_suffix(eid: str):
128
130
 
129
131
 
130
132
  def main():
133
+ try:
134
+ sys.stdout.reconfigure(encoding="utf-8")
135
+ except AttributeError: # Python < 3.7
136
+ pass
137
+
131
138
  args = sys.argv[1:]
132
139
  threshold = 0.7
133
140
  json_output = "--json" in args
@@ -154,14 +161,17 @@ def main():
154
161
  # existing-vs-existing pairs (unchanged behavior)
155
162
  for i, a in enumerate(entries):
156
163
  for b in entries[i + 1:]:
157
- if a["layer"] != b["layer"]:
158
- continue
164
+ # Exact-hash matches are reported across layers (same text is
165
+ # a duplicate no matter which layer it landed in); fuzzy
166
+ # Jaccard comparisons stay within the same layer.
159
167
  if hash_suffix(a["id"]) == hash_suffix(b["id"]):
160
168
  pairs.append((a, b, 1.0, "identical hash"))
161
169
  continue
170
+ if a["layer"] != b["layer"]:
171
+ continue
162
172
  sim = jaccard(tokenize(a["text"]), tokenize(b["text"]))
163
173
  if sim >= threshold:
164
- pairs.append((a, b, sim, f"similar text ({threshold})"))
174
+ pairs.append((a, b, sim, f"similar text (>= {threshold})"))
165
175
 
166
176
  # candidate-vs-existing pairs
167
177
  if candidates:
@@ -176,7 +186,7 @@ def main():
176
186
  )
177
187
  if sim >= threshold:
178
188
  pairs.append((candidates[0], a, sim,
179
- f"candidate similar to existing ({threshold})"))
189
+ f"candidate similar to existing (>= {threshold})"))
180
190
 
181
191
  pairs.sort(key=lambda x: -x[2])
182
192
 
@@ -8,20 +8,23 @@ Usage:
8
8
 
9
9
  Reports two categories:
10
10
 
11
- Stale : entry has not been `#verified` within the threshold
12
- (or has no #verified at all, and was added > threshold
13
- days ago).
14
- Pending arch : entry already carries a `#stale:` tag and is waiting
15
- to be moved into .lore/archive/.
11
+ Stale : entry has not been `#verified` within the threshold
12
+ (or has no #verified at all, and was added > threshold
13
+ days ago).
14
+ Pending review : entry is superseded (carries `#stale`, or carries
15
+ `#superseded-by` which implies staleness). (The skill
16
+ does not auto-archive; this category is a heads-up that
17
+ the entry is no longer accurate and should be reviewed
18
+ or left as historical record.)
16
19
 
17
20
  Output is plain text by default, JSON with --json.
18
21
 
19
22
  Used by:
20
23
  - `audit` workflow (read-only)
21
24
  - `compress` workflow (advisory)
22
- - `lore mirror` (sanity check before regenerating)
23
25
  """
24
26
  import json
27
+ import os
25
28
  import subprocess
26
29
  import sys
27
30
  from datetime import date, datetime, timedelta
@@ -34,6 +37,8 @@ def get_entries():
34
37
  [sys.executable, str(script), "--json"],
35
38
  capture_output=True,
36
39
  text=True,
40
+ encoding="utf-8",
41
+ errors="replace",
37
42
  )
38
43
  if r.returncode != 0:
39
44
  print(r.stderr.strip(), file=sys.stderr)
@@ -54,6 +59,11 @@ def parse_date(s: str):
54
59
 
55
60
 
56
61
  def main():
62
+ try:
63
+ sys.stdout.reconfigure(encoding="utf-8")
64
+ except AttributeError: # Python < 3.7
65
+ pass
66
+
57
67
  days = 90
58
68
  json_output = "--json" in sys.argv[1:]
59
69
 
@@ -66,12 +76,34 @@ def main():
66
76
 
67
77
  entries = get_entries()
68
78
  stale = []
69
- pending_arch = []
79
+ pending_review = []
80
+
81
+ # Build a quick lookup for chain validation.
82
+ by_id = {e["id"]: e for e in entries}
83
+
84
+ broken_chains = []
85
+ pending_by_chain = {} # replaced_by -> [entry, ...]
70
86
 
71
87
  for e in entries:
72
- # Already marked stale pending archive
73
- if "stale" in e["tags"]:
74
- pending_arch.append(e)
88
+ # Superseded (tagged #stale, or carrying #superseded-by which
89
+ # implies staleness per references/entry-format.md) -> pending
90
+ # review (and maybe broken chain)
91
+ if "stale" in e["tags"] or e.get("replaced_by"):
92
+ target = e.get("replaced_by")
93
+ if target and target not in by_id:
94
+ broken_chains.append({
95
+ "id": e["id"],
96
+ "file": e["file"],
97
+ "text": e["text"],
98
+ "missing_target": target,
99
+ })
100
+ if target:
101
+ pending_by_chain.setdefault(target, []).append(e)
102
+ else:
103
+ # No chain info — keep under a sentinel so the existing
104
+ # output still includes it.
105
+ pending_by_chain.setdefault(None, []).append(e)
106
+ pending_review.append(e)
75
107
  continue
76
108
 
77
109
  # Determine the entry's freshness date
@@ -90,7 +122,11 @@ def main():
90
122
  "threshold_days": days,
91
123
  "as_of": today.isoformat(),
92
124
  "stale": stale,
93
- "pending_archive": pending_arch,
125
+ "pending_review": pending_review,
126
+ "chains": {target: [e["id"] for e in entries_]
127
+ for target, entries_ in pending_by_chain.items()
128
+ if target is not None},
129
+ "broken_chains": broken_chains,
94
130
  }
95
131
  print(json.dumps(out, indent=2, ensure_ascii=False))
96
132
  return
@@ -104,12 +140,28 @@ def main():
104
140
  print(f" ref date: {ref}")
105
141
 
106
142
  print()
107
- print("=== Pending archive (tagged #stale) ===")
108
- if not pending_arch:
143
+ print("=== Pending review (tagged #stale, grouped by replacement) ===")
144
+ if not pending_by_chain:
109
145
  print(" (none)")
110
- for e in pending_arch:
111
- print(f" [{e['file']}] {e['id']} {e['text']}")
112
- print(f" marked stale: {e['tags']['stale']}")
146
+ for target, entries_ in sorted(
147
+ pending_by_chain.items(), key=lambda kv: (kv[0] is None, kv[0] or "")
148
+ ):
149
+ if target is None:
150
+ print(" (no #superseded-by chain):")
151
+ else:
152
+ print(f" -> superseded-by {target}:")
153
+ for e in entries_:
154
+ chain = (
155
+ f" -> {e['replaced_by']}" if e.get("replaced_by") else ""
156
+ )
157
+ print(f" [{e['file']}] {e['id']} {e['text']}{chain}")
158
+
159
+ if broken_chains:
160
+ print()
161
+ print("=== Broken chains (#superseded-by target not found) ===")
162
+ for b in broken_chains:
163
+ print(f" [{b['file']}] {b['id']} {b['text']}")
164
+ print(f" missing: {b['missing_target']}")
113
165
 
114
166
 
115
167
  if __name__ == "__main__":
@@ -14,6 +14,7 @@ import re
14
14
  import subprocess
15
15
  import sys
16
16
  from pathlib import Path
17
+ import os as _os
17
18
  import json as _json # standard library; aliased to avoid clashing with future vars
18
19
 
19
20
 
@@ -21,6 +22,34 @@ import json as _json # standard library; aliased to avoid clashing with future
21
22
  ENTRY_ID_RE = re.compile(r"^[A-Z]+-\d{4}-\d{2}-\d{2}-[a-f0-9]{4}$")
22
23
 
23
24
 
25
+ # Date-only ISO pattern (YYYY-MM-DD). Used to detect inputs that need
26
+ # normalization before being passed to `git log --since=` (see below).
27
+ _DATE_ONLY_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
28
+
29
+
30
+ def normalize_since(value):
31
+ """Normalize a `--since` value to a precise timestamp before git log.
32
+
33
+ `git log --since=YYYY-MM-DD` interpretation is version-dependent:
34
+ older git versions parse it as the user's local-timezone midnight,
35
+ newer versions as UTC midnight. A commit made early in the day in
36
+ any timezone can therefore be silently dropped when filtering by a
37
+ same-day `#added` tag.
38
+
39
+ Defense: when the input is date-only (no time component), append
40
+ `T00:00:00` so git's date parser treats it as a precise timestamp.
41
+ Strings that already contain a time component (T or space) are
42
+ passed through unchanged. None passes through unchanged.
43
+
44
+ Refs: https://git-scm.com/docs/git-log#_date_formats
45
+ """
46
+ if value is None or not isinstance(value, str):
47
+ return value
48
+ if not _DATE_ONLY_RE.match(value):
49
+ return value
50
+ return value + "T00:00:00"
51
+
52
+
24
53
  def parse_arg(arg: str):
25
54
  """Dispatch the first positional argument to entry / file / scope form.
26
55
 
@@ -257,6 +286,7 @@ def render_json(meta, commits):
257
286
  "code_file": meta["code_file"],
258
287
  "since": meta["since"],
259
288
  "since_source": meta["since_source"],
289
+ "chain": meta.get("chain"),
260
290
  "commits": commits,
261
291
  }
262
292
  return _json.dumps(payload, indent=2, ensure_ascii=False)
@@ -267,15 +297,32 @@ def render_markdown(meta, commits):
267
297
 
268
298
  Args:
269
299
  meta: dict with keys entry_id, lore_file, code_file, since,
270
- since_source.
300
+ since_source, and optionally _chain_entries (raw entry dicts).
271
301
  commits: list of commit dicts (see parse_commit_line + extract_refs).
272
302
 
273
303
  Returns:
274
304
  Markdown string ready for stdout.
275
305
  """
276
306
  lines = []
277
- lines.append(f"# history: [{meta['entry_id']}]")
307
+ title_suffix = ""
308
+ if meta.get("_chain_entries"):
309
+ title_suffix = " --follow-superseded"
310
+ lines.append(f"# history: [{meta['entry_id']}]{title_suffix}")
278
311
  lines.append("")
312
+
313
+ chain_entries = meta.get("_chain_entries")
314
+ if chain_entries:
315
+ lines.append("## Chain")
316
+ for idx, e in enumerate(chain_entries, start=1):
317
+ next_link = (
318
+ f"\n -> superseded-by -> {e.get('replaced_by')}"
319
+ if e.get("replaced_by") else "\n -> no successor"
320
+ )
321
+ lines.append(
322
+ f"{idx}. [{e['id']}] ({e['file']}) - {e['text']}{next_link}"
323
+ )
324
+ lines.append("")
325
+
279
326
  lines.append(f"> Entry: {meta['lore_file']}")
280
327
  since_suffix = " (entry #added date)" if meta.get("since_source") == "entry_added" else ""
281
328
  lines.append(f"> Since: {meta['since']}{since_suffix}")
@@ -401,24 +448,68 @@ def _enrich_commits_with_body_and_refs(project_root, commits):
401
448
  c["refs"] = extract_refs(msg)
402
449
 
403
450
 
451
+ def walk_supersede_chain(entries_by_id, start_id, max_depth=20):
452
+ """Follow #superseded-by links forward from start_id.
453
+
454
+ Returns a list of entry dicts [start, successor1, successor2, ...].
455
+ Stops when an entry has no replaced_by tag, the target is missing,
456
+ or max_depth is reached (cycle protection).
457
+
458
+ `entries_by_id` is a dict {id: entry_dict} from list_entries.py --json.
459
+ """
460
+ chain = []
461
+ seen = set()
462
+ current_id = start_id
463
+ for _ in range(max_depth):
464
+ if current_id in seen:
465
+ break # cycle; don't loop forever
466
+ seen.add(current_id)
467
+ entry = entries_by_id.get(current_id)
468
+ if entry is None:
469
+ break
470
+ chain.append(entry)
471
+ next_id = entry.get("replaced_by")
472
+ if not next_id:
473
+ break
474
+ current_id = next_id
475
+ return chain
476
+
477
+
404
478
  def main():
405
479
  args = sys.argv[1:]
480
+ try:
481
+ sys.stdout.reconfigure(encoding="utf-8")
482
+ except AttributeError: # Python < 3.7
483
+ pass
406
484
  json_mode = "--json" in args
485
+ follow_superseded = "--follow-superseded" in args
407
486
  since_override = None
408
487
  for a in args:
409
488
  if a.startswith("--since="):
410
489
  since_override = a.split("=", 1)[1]
411
490
 
412
- positional = [a for a in args if a != "--json" and not a.startswith("--since=")]
491
+ positional = [
492
+ a for a in args
493
+ if a != "--json"
494
+ and a != "--follow-superseded"
495
+ and not a.startswith("--since=")
496
+ ]
413
497
  if not positional:
414
- print("usage: lore history <entry-id|file-path|--scope=NAME>",
415
- file=sys.stderr)
498
+ print(
499
+ "usage: lore history <entry-id|file-path|--scope=NAME> "
500
+ "[--follow-superseded] [--since=YYYY-MM-DD] [--json]",
501
+ file=sys.stderr,
502
+ )
416
503
  die(ERR_USAGE, "missing argument")
417
504
 
418
505
  parsed = parse_arg(positional[0])
419
506
  if parsed is None:
420
507
  die(ERR_USAGE, f"unrecognized argument: {positional[0]}")
421
508
 
509
+ if follow_superseded and parsed["form"] != "entry":
510
+ print("[WARN] --follow-superseded only applies to the entry form; ignored.",
511
+ file=sys.stderr)
512
+
422
513
  project_root = _find_lore_root_or_die()
423
514
 
424
515
  if not _is_git_repo(project_root):
@@ -428,33 +519,147 @@ def main():
428
519
 
429
520
  if parsed["form"] == "entry":
430
521
  entries = _load_entries_via_subprocess()
522
+ entries_by_id = {e["id"]: e for e in entries}
431
523
  entry = find_entry(entries, parsed["value"])
432
524
  if entry is None:
433
525
  ids = ", ".join(e["id"] for e in entries[:20])
434
526
  more = "" if len(entries) <= 20 else f" (and {len(entries)-20} more)"
435
527
  die(ERR_NO_ENTRY,
436
528
  f"Entry {parsed['value']} not found. Available: {ids}{more}")
437
- since = since_override or extract_added_date(entry.get("tags", {}))
438
- if since is None:
439
- print("warning: entry has no #added tag; using full history",
440
- file=sys.stderr)
441
- since = "1970-01-01"
442
- code_file = resolve_code_file(entry)
443
- try:
444
- commits = run_git_log(project_root, since, code_file)
445
- except RuntimeError as exc:
446
- die(ERR_GIT_FAIL, str(exc))
447
- _enrich_commits_with_body_and_refs(project_root, commits)
448
- meta = _build_meta_entry(entry, code_file, since, "entry_added")
449
- out = render_json(meta, commits) if json_mode else render_markdown(meta, commits)
450
- print(out)
529
+
530
+ chain = ([entry] if not follow_superseded
531
+ else walk_supersede_chain(entries_by_id, entry["id"]))
532
+
533
+ if not follow_superseded:
534
+ since = since_override or extract_added_date(entry.get("tags", {}))
535
+ use_full_history = since is None
536
+ if since is None:
537
+ print("warning: entry has no #added tag; using full history",
538
+ file=sys.stderr)
539
+ since = "1970-01-01"
540
+ since = normalize_since(since)
541
+ code_file = resolve_code_file(entry)
542
+ try:
543
+ commits = run_git_log(
544
+ project_root,
545
+ None if use_full_history else since,
546
+ code_file,
547
+ )
548
+ except RuntimeError as exc:
549
+ die(ERR_GIT_FAIL, str(exc))
550
+ _enrich_commits_with_body_and_refs(project_root, commits)
551
+ since_source = "user_arg" if since_override else "entry_added"
552
+ meta = _build_meta_entry(entry, code_file, since, since_source)
553
+ meta["chain"] = None
554
+ meta["_chain_entries"] = None
555
+ out = render_json(meta, commits) if json_mode else render_markdown(meta, commits)
556
+ print(out)
557
+ return
558
+
559
+ # --follow-superseded: iterate over the entire chain and collect
560
+ # per-entry logs. Each successor may point at a different file/date.
561
+ per_entry = []
562
+ for idx, e in enumerate(chain):
563
+ if idx == 0 and since_override is not None:
564
+ e_since_raw = since_override
565
+ e_since_source = "user_arg"
566
+ else:
567
+ e_since_raw = extract_added_date(e.get("tags", {}))
568
+ use_full_history = e_since_raw is None
569
+ if e_since_raw is None:
570
+ print(f"warning: entry {e['id']} has no #added tag; using full history",
571
+ file=sys.stderr)
572
+ e_since_raw = "1970-01-01"
573
+ e_since_source = "entry_added"
574
+ if idx == 0 and since_override is not None:
575
+ use_full_history = False
576
+ e_since = normalize_since(e_since_raw)
577
+ e_code_file = resolve_code_file(e)
578
+ try:
579
+ e_commits = run_git_log(
580
+ project_root,
581
+ None if use_full_history else e_since,
582
+ e_code_file,
583
+ )
584
+ except RuntimeError as exc:
585
+ die(ERR_GIT_FAIL, str(exc))
586
+ _enrich_commits_with_body_and_refs(project_root, e_commits)
587
+ per_entry.append((e, e_since, e_code_file, e_since_source, e_commits))
588
+
589
+ if json_mode:
590
+ chain_meta = [
591
+ {
592
+ "entry_id": e["id"],
593
+ "lore_file": e["file"],
594
+ "code_file": cf,
595
+ "since": s,
596
+ }
597
+ for (e, s, cf, _, _) in per_entry
598
+ ]
599
+ results = [
600
+ {
601
+ "entry_id": e["id"],
602
+ "lore_file": e["file"],
603
+ "code_file": cf,
604
+ "since": s,
605
+ "since_source": ss,
606
+ "commits": commits,
607
+ }
608
+ for (e, s, cf, ss, commits) in per_entry
609
+ ]
610
+ # Top-level keeps first entry's fields for backward compat
611
+ first_e, first_since, first_cf, first_ss, first_commits = per_entry[0]
612
+ payload = {
613
+ "entry_id": first_e["id"],
614
+ "lore_file": first_e["file"],
615
+ "code_file": first_cf,
616
+ "since": first_since,
617
+ "since_source": first_ss,
618
+ "chain": chain_meta,
619
+ "commits": first_commits,
620
+ "results": results,
621
+ }
622
+ # Preserve _chain_entries style for any downstream that expects it
623
+ payload["_chain_entries"] = None
624
+ print(_json.dumps(payload, indent=2, ensure_ascii=False))
625
+ return
626
+
627
+ # Markdown: Chain section then per-entry blocks
628
+ out_lines = []
629
+ out_lines.append(f"# history: [{chain[0]['id']}] --follow-superseded")
630
+ out_lines.append("")
631
+ out_lines.append("## Chain")
632
+ for idx, (e, _, _, _, _) in enumerate(per_entry, start=1):
633
+ next_link = (
634
+ f"\n -> superseded-by -> {e.get('replaced_by')}"
635
+ if e.get("replaced_by") else "\n -> no successor"
636
+ )
637
+ out_lines.append(
638
+ f"{idx}. [{e['id']}] ({e['file']}) - {e['text']}{next_link}"
639
+ )
640
+ out_lines.append("")
641
+ for (e, e_since, e_code_file, e_since_source, e_commits) in per_entry:
642
+ meta = _build_meta_entry(e, e_code_file, e_since, e_since_source)
643
+ meta["_chain_entries"] = None
644
+ # Render each entry's block without re-adding the Chain header
645
+ block = render_markdown(meta, e_commits)
646
+ # render_markdown starts with "# history: [id]" — keep it, but
647
+ # the top already has the --follow-superseded title.
648
+ out_lines.append(block.rstrip())
649
+ out_lines.append("")
650
+ print("\n".join(out_lines).rstrip() + "\n")
451
651
  return
452
652
 
453
653
  if parsed["form"] == "file":
454
654
  since = since_override or "1970-01-01"
655
+ since = normalize_since(since)
455
656
  code_file = parsed["value"]
456
657
  try:
457
- commits = run_git_log(project_root, since, code_file)
658
+ commits = run_git_log(
659
+ project_root,
660
+ since if since_override else None,
661
+ code_file,
662
+ )
458
663
  except RuntimeError as exc:
459
664
  die(ERR_GIT_FAIL, str(exc))
460
665
  _enrich_commits_with_body_and_refs(project_root, commits)
@@ -472,13 +677,20 @@ def main():
472
677
  if parsed["form"] == "scope":
473
678
  layer_files = _resolve_scope_to_md_files(project_root, parsed["value"])
474
679
  scope_payloads = [] # only used when json_mode is True
680
+ scope_since = normalize_since(since_override or "1970-01-01")
475
681
  for layer_name, md_path in layer_files:
476
682
  # For scope form we treat each .md file as a "code file" stand-in:
477
683
  # we git log the md file's project-relative path to find commits
478
684
  # that touched that lore file. (Useful for tracking lore edits.)
479
- rel = str(md_path.relative_to(project_root))
685
+ rel = str(md_path.relative_to(project_root)).replace(
686
+ _os.sep, "/"
687
+ )
480
688
  try:
481
- commits = run_git_log(project_root, "1970-01-01", rel)
689
+ commits = run_git_log(
690
+ project_root,
691
+ scope_since if since_override else None,
692
+ rel,
693
+ )
482
694
  except RuntimeError as exc:
483
695
  die(ERR_GIT_FAIL, str(exc))
484
696
  _enrich_commits_with_body_and_refs(project_root, commits)
@@ -487,7 +699,7 @@ def main():
487
699
  "entry_id": f"<scope:{parsed['value']}/{layer_name}>",
488
700
  "lore_file": rel,
489
701
  "code_file": rel,
490
- "since": "1970-01-01",
702
+ "since": scope_since,
491
703
  "since_source": "scope_form",
492
704
  }
493
705
  scope_payloads.append({
@@ -7,10 +7,13 @@ Usage:
7
7
  Output:
8
8
  The 4-char lowercase hex hash that goes into an entry's ID, e.g. `a3f2`.
9
9
 
10
- The hash is `sha256(text).hexdigest()[:4]`. This is the same algorithm
11
- described in `references/entry-format.md` (ID generation section), so
12
- running this script always produces the ID component a lore agent
13
- would assign.
10
+ The hash is `sha256(text).hexdigest()[:4]`. This matches the algorithm
11
+ in `references/entry-format.md` and what `list_entries.py` uses when
12
+ re-reading an entry from disk (it strips `#tag:value` pairs before
13
+ hashing). To stay consistent, **pass the entry body WITHOUT the inline
14
+ tags** — i.e. the text that goes between the `[ID]` and the first `#`.
15
+ Including tags in the input here will produce a different hash than the
16
+ file's stored ID, breaking round-trip verification.
14
17
 
15
18
  Cross-platform: works on Windows / Linux / macOS with Python 3.6+.
16
19
  """