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.
- package/bundled-skills/.antigravity-install-manifest.json +5 -2
- package/bundled-skills/animejs-animation/SKILL.md +1 -1
- package/bundled-skills/anti-ui-slop/SKILL.md +51 -86
- package/bundled-skills/antigravity-design-expert/SKILL.md +1 -1
- package/bundled-skills/design-spells/SKILL.md +1 -1
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +1 -1
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/aas-core.md +1 -1
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +3 -3
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/famulor-skill/SKILL.md +132 -0
- package/bundled-skills/famulor-skill/references/assistant-design.md +137 -0
- package/bundled-skills/famulor-skill/references/toolsets/assistants.md +64 -0
- package/bundled-skills/famulor-skill/references/toolsets/automations.md +36 -0
- package/bundled-skills/famulor-skill/references/toolsets/billing.md +15 -0
- package/bundled-skills/famulor-skill/references/toolsets/calls.md +23 -0
- package/bundled-skills/famulor-skill/references/toolsets/campaigns.md +42 -0
- package/bundled-skills/famulor-skill/references/toolsets/dashboards.md +27 -0
- package/bundled-skills/famulor-skill/references/toolsets/knowledge.md +28 -0
- package/bundled-skills/famulor-skill/references/toolsets/messaging.md +52 -0
- package/bundled-skills/famulor-skill/references/toolsets/migration.md +10 -0
- package/bundled-skills/famulor-skill/references/toolsets/platform.md +14 -0
- package/bundled-skills/famulor-skill/references/toolsets/settings.md +28 -0
- package/bundled-skills/famulor-skill/references/toolsets/tasks.md +12 -0
- package/bundled-skills/famulor-skill/references/toolsets/telephony.md +35 -0
- package/bundled-skills/iconsax-library/SKILL.md +1 -1
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package-lock.json +4 -4
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package.json +1 -1
- package/bundled-skills/lore/SKILL.md +104 -298
- package/bundled-skills/lore/references/audit-template.md +21 -4
- package/bundled-skills/lore/references/compatibility.md +51 -121
- package/bundled-skills/lore/references/config.md +22 -23
- package/bundled-skills/lore/references/entry-format.md +33 -3
- package/bundled-skills/lore/references/history-command.md +98 -2
- package/bundled-skills/lore/references/platform-mirrors.md +45 -16
- package/bundled-skills/lore/references/stale-new-markers.md +10 -8
- package/bundled-skills/lore/references/summary-template.md +8 -1
- package/bundled-skills/lore/references/workflows.md +192 -0
- package/bundled-skills/lore/scripts/README.md +14 -13
- package/bundled-skills/lore/scripts/README.zh-CN.md +14 -13
- package/bundled-skills/lore/scripts/find_duplicates.py +14 -4
- package/bundled-skills/lore/scripts/find_stale.py +68 -16
- package/bundled-skills/lore/scripts/history.py +235 -23
- package/bundled-skills/lore/scripts/id_hash.py +7 -4
- package/bundled-skills/lore/scripts/list_entries.py +82 -13
- package/bundled-skills/magic-animator/SKILL.md +1 -1
- package/bundled-skills/magic-ui-generator/SKILL.md +1 -1
- package/bundled-skills/poka-yoke/SKILL.md +172 -0
- package/bundled-skills/project-state-governor/SKILL.md +483 -0
- package/bundled-skills/project-state-governor/references/manifest-routing.md +47 -0
- package/bundled-skills/project-state-governor/references/persistence-lifecycle.md +93 -0
- package/bundled-skills/project-state-governor/references/project-state-schema.md +173 -0
- package/bundled-skills/project-state-governor/references/reconstruction-workflow.md +183 -0
- package/bundled-skills/salesforce-development/SKILL.md +107 -37
- package/bundled-skills/spec-driven-loop/SKILL.md +203 -0
- package/bundled-skills/spec-driven-loop/references/agent-and-judge-contracts.md +120 -0
- package/bundled-skills/spec-driven-loop/references/document-templates.md +184 -0
- package/bundled-skills/unsplash-integration/SKILL.md +1 -1
- package/bundled-skills/vizcom/SKILL.md +1 -1
- package/bundled-skills/x-twitter-scraper/SKILL.md +20 -7
- package/package.json +1 -1
- package/skills_index.json +136 -36
- package/bundled-skills/lore/README.md +0 -386
- package/bundled-skills/lore/README.zh-CN.md +0 -386
- package/bundled-skills/lore/WORKFLOWS.md +0 -216
- package/bundled-skills/lore/WORKFLOWS.zh-CN.md +0 -216
- 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
|
-
|
|
158
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Pending
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
"
|
|
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
|
|
108
|
-
if not
|
|
143
|
+
print("=== Pending review (tagged #stale, grouped by replacement) ===")
|
|
144
|
+
if not pending_by_chain:
|
|
109
145
|
print(" (none)")
|
|
110
|
-
for
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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 = [
|
|
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(
|
|
415
|
-
|
|
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
|
-
|
|
438
|
-
if
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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(
|
|
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(
|
|
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":
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
"""
|