opencode-skills-collection 4.0.0 → 4.0.1
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 +24 -1
- package/bundled-skills/007/SKILL.md +2 -2
- package/bundled-skills/10-andruia-skill-smith/SKILL.md +1 -1
- package/bundled-skills/agent-squad/SKILL.md +5 -0
- package/bundled-skills/agent-squad/alex/SKILL.md +3 -0
- package/bundled-skills/agent-squad/aria/SKILL.md +3 -0
- package/bundled-skills/agent-squad/dep/SKILL.md +3 -0
- package/bundled-skills/agent-squad/luna/SKILL.md +4 -1
- package/bundled-skills/agent-squad/mason/SKILL.md +3 -0
- package/bundled-skills/agent-squad/max/SKILL.md +3 -0
- package/bundled-skills/agent-squad/quinn/SKILL.md +3 -0
- package/bundled-skills/agent-squad/rex/SKILL.md +3 -0
- package/bundled-skills/angular/metadata.json +1 -1
- package/bundled-skills/angular-best-practices/metadata.json +1 -1
- package/bundled-skills/angular-state-management/metadata.json +1 -1
- package/bundled-skills/angular-ui-patterns/metadata.json +1 -1
- package/bundled-skills/antigravity-skill-orchestrator/SKILL.md +2 -2
- package/bundled-skills/apify-actor-development/SKILL.md +2 -2
- package/bundled-skills/apple-container/SKILL.md +165 -0
- package/bundled-skills/apple-container/references/commands.md +1593 -0
- package/bundled-skills/apple-container/references/concepts.md +294 -0
- package/bundled-skills/apple-container/references/configuration.md +499 -0
- package/bundled-skills/apple-container/references/workflows.md +498 -0
- package/bundled-skills/ask-copilot/SKILL.md +112 -0
- package/bundled-skills/auto-research/SKILL.md +126 -0
- package/bundled-skills/brooks-lint/SKILL.md +1 -1
- package/bundled-skills/browser-harness/references/install.md +3 -1
- package/bundled-skills/chrome-extension-developer/SKILL.md +1 -1
- package/bundled-skills/clean-code-guard/SKILL.md +184 -0
- package/bundled-skills/clean-code-guard/references/ai-failure-modes.md +345 -0
- package/bundled-skills/clean-code-guard/references/comments-and-formatting.md +115 -0
- package/bundled-skills/clean-code-guard/references/dry-kiss-yagni.md +133 -0
- package/bundled-skills/clean-code-guard/references/naming-and-functions.md +187 -0
- package/bundled-skills/clean-code-guard/references/review-checklist.md +150 -0
- package/bundled-skills/clean-code-guard/references/solid.md +280 -0
- package/bundled-skills/clean-code-guard/references/sources.md +47 -0
- package/bundled-skills/cloudflare-security-audit/SKILL.md +137 -0
- package/bundled-skills/cloudflare-security-audit/references/AI-AND-LLM.md +67 -0
- package/bundled-skills/cloudflare-security-audit/references/ATTACK-CLASSES.md +116 -0
- package/bundled-skills/cloudflare-security-audit/references/CLIENT-SIDE.md +67 -0
- package/bundled-skills/cloudflare-security-audit/references/HUNTING.md +110 -0
- package/bundled-skills/cloudflare-security-audit/references/MEMORY-SAFETY-AND-BINARY.md +58 -0
- package/bundled-skills/cloudflare-security-audit/references/RECONNAISSANCE.md +46 -0
- package/bundled-skills/cloudflare-security-audit/references/VALIDATION-AND-REPORTING.md +109 -0
- package/bundled-skills/cloudflare-security-audit/references/WEB-PROTOCOL-AND-AUTH.md +85 -0
- package/bundled-skills/cloudflare-security-audit/resources/report-schema.json +210 -0
- package/bundled-skills/comfyui-gateway/references/troubleshooting.md +1 -1
- package/bundled-skills/commit/SKILL.md +2 -1
- package/bundled-skills/deepapi/SKILL.md +6 -6
- package/bundled-skills/docs/README.md +1 -1
- package/bundled-skills/docs/contributors/quality-bar.md +1 -1
- package/bundled-skills/docs/contributors/security-guardrails.md +1 -1
- package/bundled-skills/docs/integrations/jetski-cortex.md +5 -5
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +3 -3
- package/bundled-skills/docs/maintainers/ci-drift-fix.md +7 -5
- package/bundled-skills/docs/maintainers/merge-batch.md +24 -7
- package/bundled-skills/docs/maintainers/pr-autonomy.md +89 -0
- package/bundled-skills/docs/maintainers/release-notes-7.2.0.md +1 -1
- package/bundled-skills/docs/maintainers/release-process.md +9 -9
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +5 -5
- package/bundled-skills/docs/maintainers/skills-date-tracking.md +1 -1
- package/bundled-skills/docs/maintainers/skills-update-guide.md +2 -2
- package/bundled-skills/docs/users/{antigravity-awesome-skills-vs-awesome-claude-skills.md → agentic-awesome-skills-vs-awesome-claude-skills.md} +5 -5
- package/bundled-skills/docs/users/ai-agent-skills.md +4 -4
- package/bundled-skills/docs/users/best-claude-code-skills-github.md +3 -3
- package/bundled-skills/docs/users/best-cursor-skills-github.md +3 -3
- package/bundled-skills/docs/users/bundles.md +56 -57
- package/bundled-skills/docs/users/claude-code-skills.md +6 -6
- package/bundled-skills/docs/users/codex-cli-skills.md +5 -5
- package/bundled-skills/docs/users/cursor-skills.md +3 -3
- package/bundled-skills/docs/users/faq.md +27 -15
- package/bundled-skills/docs/users/gemini-cli-skills.md +4 -4
- package/bundled-skills/docs/users/getting-started.md +22 -12
- package/bundled-skills/docs/users/kiro-integration.md +7 -7
- package/bundled-skills/docs/users/plugins.md +8 -9
- package/bundled-skills/docs/users/security-skills.md +1 -1
- package/bundled-skills/docs/users/skills-vs-mcp-tools.md +1 -1
- package/bundled-skills/docs/users/specialized-plugin-roadmap.md +3 -4
- package/bundled-skills/docs/users/usage.md +9 -9
- package/bundled-skills/docs/users/visual-guide.md +7 -7
- package/bundled-skills/docs/users/windows-truncation-recovery.md +1 -1
- package/bundled-skills/docs/users/workflows.md +1 -1
- package/bundled-skills/docs/vietnamese/BUNDLES.vi.md +1 -1
- package/bundled-skills/docs/vietnamese/FAQ.vi.md +15 -8
- package/bundled-skills/docs/vietnamese/GETTING_STARTED.vi.md +12 -5
- package/bundled-skills/docs/vietnamese/QUALITY_BAR.vi.md +1 -1
- package/bundled-skills/docs/vietnamese/README.vi.md +21 -17
- package/bundled-skills/docs/vietnamese/SECURITY_GUARDRAILS.vi.md +1 -1
- package/bundled-skills/docs/vietnamese/SKILLS_README.vi.md +2 -2
- package/bundled-skills/docs/vietnamese/TRANSLATION_PLAN.vi.md +1 -1
- package/bundled-skills/docs/vietnamese/VISUAL_GUIDE.vi.md +8 -8
- package/bundled-skills/docs-guard/SKILL.md +109 -0
- package/bundled-skills/docs-guard/references/code-samples.md +46 -0
- package/bundled-skills/docs-guard/references/docstrings.md +60 -0
- package/bundled-skills/docs-guard/references/review-checklist.md +45 -0
- package/bundled-skills/docs-guard/references/sources.md +22 -0
- package/bundled-skills/docs-guard/references/verification.md +51 -0
- package/bundled-skills/efficient-web-research/SKILL.md +4 -0
- package/bundled-skills/electron-development/SKILL.md +1 -1
- package/bundled-skills/event-staffing-compliance/SKILL.md +3 -0
- package/bundled-skills/event-staffing-ordering/SKILL.md +3 -0
- package/bundled-skills/feature-tracking/SKILL.md +222 -0
- package/bundled-skills/flutter-expert/SKILL.md +6 -1
- package/bundled-skills/flutter-expert/resources/implementation-playbook.md +214 -0
- package/bundled-skills/gemini-deep-research/README.md +246 -0
- package/bundled-skills/gemini-deep-research/SKILL.md +136 -0
- package/bundled-skills/gemini-deep-research/requirements.txt +2 -0
- package/bundled-skills/gemini-deep-research/scripts/research.py +692 -0
- package/bundled-skills/git-pushing/SKILL.md +21 -4
- package/bundled-skills/git-pushing/scripts/smart_commit.sh +206 -11
- package/bundled-skills/go-in-depth/scripts/workflow-script.js +4 -1
- package/bundled-skills/grok-build/SKILL.md +167 -0
- package/bundled-skills/grok-build/references/cli.md +99 -0
- package/bundled-skills/huggingface-tool-builder/references/find_models_by_paper.sh +1 -1
- package/bundled-skills/idea-autopsy/SKILL.md +147 -0
- package/bundled-skills/it-manager-hospital/README.md +1 -1
- package/bundled-skills/it-manager-pro/README.md +1 -1
- package/bundled-skills/itil-expert/README.md +1 -1
- package/bundled-skills/langgraph/SKILL.md +1 -1
- package/bundled-skills/linkedin-content-generator/SKILL.md +1 -1
- package/bundled-skills/lint-and-validate/scripts/lint_runner.py +23 -23
- package/bundled-skills/lint-and-validate/scripts/type_coverage.py +30 -30
- package/bundled-skills/logic-lens/SKILL.md +1 -1
- package/bundled-skills/lore/LICENSE +21 -0
- package/bundled-skills/lore/README.md +386 -0
- package/bundled-skills/lore/README.zh-CN.md +386 -0
- package/bundled-skills/lore/SKILL.md +453 -0
- package/bundled-skills/lore/WORKFLOWS.md +216 -0
- package/bundled-skills/lore/WORKFLOWS.zh-CN.md +216 -0
- package/bundled-skills/lore/references/audit-template.md +60 -0
- package/bundled-skills/lore/references/compatibility.md +228 -0
- package/bundled-skills/lore/references/config.md +125 -0
- package/bundled-skills/lore/references/entry-format.md +70 -0
- package/bundled-skills/lore/references/history-command.md +106 -0
- package/bundled-skills/lore/references/monorepo-detection.md +78 -0
- package/bundled-skills/lore/references/platform-mirrors.md +377 -0
- package/bundled-skills/lore/references/stale-new-markers.md +62 -0
- package/bundled-skills/lore/references/summary-template.md +98 -0
- package/bundled-skills/lore/scripts/README.md +54 -0
- package/bundled-skills/lore/scripts/README.zh-CN.md +54 -0
- package/bundled-skills/lore/scripts/find_duplicates.py +211 -0
- package/bundled-skills/lore/scripts/find_stale.py +116 -0
- package/bundled-skills/lore/scripts/history.py +527 -0
- package/bundled-skills/lore/scripts/id_hash.py +32 -0
- package/bundled-skills/lore/scripts/list_entries.py +183 -0
- package/bundled-skills/lovable-cleanup/SKILL.md +3 -3
- package/bundled-skills/makepad-splash/SKILL.md +1 -1
- package/bundled-skills/mmx-cli/SKILL.md +14 -1
- package/bundled-skills/monopoly/SKILL.md +5 -0
- package/bundled-skills/monopoly/patterns/SKILL.md +4 -0
- package/bundled-skills/monopoly/scale-benchmarks/SKILL.md +4 -0
- package/bundled-skills/monopoly/security-checklist/SKILL.md +4 -0
- package/bundled-skills/monopoly/tech-matrix/SKILL.md +4 -0
- package/bundled-skills/mtls-configuration/SKILL.md +1 -1
- package/bundled-skills/network-101/SKILL.md +4 -5
- package/bundled-skills/nika/SKILL.md +270 -0
- package/bundled-skills/not-a-vibe-coder/SKILL.md +4 -0
- package/bundled-skills/ontoly-software-graph/SKILL.md +160 -0
- package/bundled-skills/openclaw-github-repo-commander/SKILL.md +1 -1
- package/bundled-skills/pilot-protocol/SKILL.md +6 -3
- package/bundled-skills/postgres-readonly-queries/README.md +77 -0
- package/bundled-skills/postgres-readonly-queries/SKILL.md +152 -0
- package/bundled-skills/postgres-readonly-queries/connections.example.json +34 -0
- package/bundled-skills/postgres-readonly-queries/requirements.txt +1 -0
- package/bundled-skills/postgres-readonly-queries/scripts/query.py +262 -0
- package/bundled-skills/pptx-deck-creation/SKILL.md +275 -0
- package/bundled-skills/pptx-deck-creation/references/audit-checklist.md +156 -0
- package/bundled-skills/pptx-deck-creation/references/design-profiles.md +96 -0
- package/bundled-skills/pptx-deck-creation/references/python-snippets.md +150 -0
- package/bundled-skills/pptx-deck-creation/references/reference-deck-analysis.md +91 -0
- package/bundled-skills/pptx-deck-creation/references/visual-asset-adapters.md +158 -0
- package/bundled-skills/privilege-escalation-methods/SKILL.md +1 -1
- package/bundled-skills/product-decision-agent/SKILL.md +94 -0
- package/bundled-skills/product-decision-agent/agents/openai.yaml +7 -0
- package/bundled-skills/product-decision-agent/references/methodology-basis.md +232 -0
- package/bundled-skills/product-decision-agent/references/product-playbooks.md +813 -0
- package/bundled-skills/product-decision-agent/references/reasoning-engine.md +289 -0
- package/bundled-skills/product-decision-agent/references/response-examples.md +279 -0
- package/bundled-skills/product-decision-agent/scripts/quality_gate.py +221 -0
- package/bundled-skills/product-decision-agent/scripts/test_quality_gate.py +94 -0
- package/bundled-skills/pytest-skill/reference/playbook.md +358 -0
- package/bundled-skills/quit-sponsor/SKILL.md +96 -0
- package/bundled-skills/rclone-cli/references/providers/s3.md +1 -1
- package/bundled-skills/security-and-hardening/SKILL.md +1 -1
- package/bundled-skills/semgrep-rule-creator/SKILL.md +6 -6
- package/bundled-skills/skill-creator/README.md +3 -3
- package/bundled-skills/skill-creator/SKILL.md +8 -4
- package/bundled-skills/skill-creator/scripts/quick_validate.py +1 -1
- package/bundled-skills/sql-injection-testing/SKILL.md +4 -4
- package/bundled-skills/subagent-orchestrator/README.md +1 -1
- package/bundled-skills/super-code/bash/SKILL.md +3 -0
- package/bundled-skills/super-code/c/SKILL.md +3 -0
- package/bundled-skills/super-code/cpp/SKILL.md +3 -0
- package/bundled-skills/super-code/csharp/SKILL.md +3 -0
- package/bundled-skills/super-code/dart/SKILL.md +3 -0
- package/bundled-skills/super-code/elixir/SKILL.md +3 -0
- package/bundled-skills/super-code/go/SKILL.md +3 -0
- package/bundled-skills/super-code/java/SKILL.md +3 -0
- package/bundled-skills/super-code/kotlin/SKILL.md +3 -0
- package/bundled-skills/super-code/php/SKILL.md +3 -0
- package/bundled-skills/super-code/python/SKILL.md +3 -0
- package/bundled-skills/super-code/ruby/SKILL.md +3 -0
- package/bundled-skills/super-code/rust/SKILL.md +3 -0
- package/bundled-skills/super-code/scala/SKILL.md +3 -0
- package/bundled-skills/super-code/swift/SKILL.md +3 -0
- package/bundled-skills/super-code/typescript/SKILL.md +3 -0
- package/bundled-skills/telegram-bot-messaging/README.md +185 -0
- package/bundled-skills/telegram-bot-messaging/SKILL.md +101 -0
- package/bundled-skills/telegram-bot-messaging/scripts/telegram.sh +420 -0
- package/bundled-skills/test-guard/SKILL.md +132 -0
- package/bundled-skills/test-guard/references/jest.md +50 -0
- package/bundled-skills/test-guard/references/llm-app-testing.md +40 -0
- package/bundled-skills/test-guard/references/phpunit.md +56 -0
- package/bundled-skills/test-guard/references/pytest.md +65 -0
- package/bundled-skills/trading-ledger/SKILL.md +11 -3
- package/bundled-skills/uizze-ui-research/SKILL.md +93 -0
- package/bundled-skills/vibe-code-auditor/SKILL.md +1 -1
- package/bundled-skills/vibecode-production-qa-validator/SKILL.md +1 -1
- package/bundled-skills/videodb/scripts/ws_listener.py +29 -15
- package/bundled-skills/voice-ai-engine-development/README.md +1 -1
- package/bundled-skills/vulnerability-scanner/SKILL.md +1 -1
- package/bundled-skills/warehouse/SKILL.md +154 -0
- package/bundled-skills/woo-guard/SKILL.md +113 -0
- package/bundled-skills/woo-guard/references/checkout-and-money.md +64 -0
- package/bundled-skills/woo-guard/references/hpos-and-crud.md +87 -0
- package/bundled-skills/woo-guard/references/review-checklist.md +44 -0
- package/bundled-skills/woo-guard/references/sources.md +17 -0
- package/bundled-skills/wp-guard/SKILL.md +119 -0
- package/bundled-skills/wp-guard/references/i18n.md +88 -0
- package/bundled-skills/wp-guard/references/performance.md +70 -0
- package/bundled-skills/wp-guard/references/review-checklist.md +57 -0
- package/bundled-skills/wp-guard/references/security.md +100 -0
- package/bundled-skills/wp-guard/references/sources.md +27 -0
- package/bundled-skills/writing-skills/gotchas.md +1 -1
- package/bundled-skills/writing-skills/references/standards/metadata-standard.md +1 -1
- package/bundled-skills/x-twitter-scraper/SKILL.md +14 -32
- package/bundled-skills/xss-html-injection/SKILL.md +3 -3
- package/bundled-skills/youtube-seo-optimizer/SKILL.md +1 -1
- package/bundled-skills/youtube-summarizer/CHANGELOG.md +2 -2
- package/package.json +1 -1
- package/skills_index.json +273 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""`lore history` — list git commits related to an entry, file, or scope.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
lore history <entry-id>
|
|
6
|
+
lore history <file-path>
|
|
7
|
+
lore history --scope=<name>
|
|
8
|
+
lore history --since=<YYYY-MM-DD>
|
|
9
|
+
lore history --json
|
|
10
|
+
|
|
11
|
+
See references/history-command.md for the full specification.
|
|
12
|
+
"""
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import json as _json # standard library; aliased to avoid clashing with future vars
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Entry ID pattern: LAYER-YYYY-MM-DD-xxxx (4 hex chars)
|
|
21
|
+
ENTRY_ID_RE = re.compile(r"^[A-Z]+-\d{4}-\d{2}-\d{2}-[a-f0-9]{4}$")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_arg(arg: str):
|
|
25
|
+
"""Dispatch the first positional argument to entry / file / scope form.
|
|
26
|
+
|
|
27
|
+
Returns a dict {"form": "entry"|"file"|"scope", "value": str}, or None
|
|
28
|
+
if the argument matches none of the recognized patterns.
|
|
29
|
+
"""
|
|
30
|
+
if not arg:
|
|
31
|
+
return None
|
|
32
|
+
if arg.startswith("--scope="):
|
|
33
|
+
return {"form": "scope", "value": arg.split("=", 1)[1]}
|
|
34
|
+
if ENTRY_ID_RE.match(arg):
|
|
35
|
+
return {"form": "entry", "value": arg}
|
|
36
|
+
if "/" in arg or arg.startswith("."):
|
|
37
|
+
return {"form": "file", "value": arg}
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def find_entry(entries, entry_id):
|
|
42
|
+
"""Look up an entry by ID in the list from list_entries.py --json.
|
|
43
|
+
|
|
44
|
+
Returns the entry dict, or None if not found.
|
|
45
|
+
"""
|
|
46
|
+
for e in entries:
|
|
47
|
+
if e.get("id") == entry_id:
|
|
48
|
+
return e
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def extract_added_date(tags):
|
|
53
|
+
"""Return the value of the 'added' tag, or None if absent.
|
|
54
|
+
|
|
55
|
+
The entry dict's `tags` field is {name: value, ...} as produced
|
|
56
|
+
by list_entries.py.
|
|
57
|
+
"""
|
|
58
|
+
if not tags:
|
|
59
|
+
return None
|
|
60
|
+
return tags.get("added")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Match a backtick-quoted path inside an entry's text. The path must
|
|
64
|
+
# contain at least one slash OR start with a dot OR end with a common
|
|
65
|
+
# code extension, to avoid false positives like `Zustand`.
|
|
66
|
+
BACKTICK_PATH_RE = re.compile(
|
|
67
|
+
r"`([^\s`]+\.[a-zA-Z0-9]{1,8}(?:\.[a-zA-Z0-9]{1,8})*"
|
|
68
|
+
r"|[^\s`]+/[^\s`]+"
|
|
69
|
+
r"|\.[a-zA-Z][^\s`]*)`"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def resolve_code_file(entry):
|
|
74
|
+
"""Decide which file path to git-log for this entry.
|
|
75
|
+
|
|
76
|
+
Priority:
|
|
77
|
+
1. First backtick-quoted path in entry.text (looks like a file).
|
|
78
|
+
2. Scope directory at project root (e.g. "frontend" for scope "frontend").
|
|
79
|
+
3. "." for the _global scope (project root).
|
|
80
|
+
|
|
81
|
+
The path returned is relative to the project root. git log handles
|
|
82
|
+
"." to mean the whole repo.
|
|
83
|
+
"""
|
|
84
|
+
if entry.get("text"):
|
|
85
|
+
m = BACKTICK_PATH_RE.search(entry["text"])
|
|
86
|
+
if m:
|
|
87
|
+
return m.group(1)
|
|
88
|
+
scope = entry.get("scope", "_global")
|
|
89
|
+
if scope == "_global":
|
|
90
|
+
return "."
|
|
91
|
+
return scope
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Single-line per commit. The trailing %s for body is multi-line content
|
|
95
|
+
# that we capture separately (not in the delimited format string) by
|
|
96
|
+
# running a second pass with a different format. For v1 we use a simple
|
|
97
|
+
# format and parse body via a follow-up `git show` only if needed.
|
|
98
|
+
#
|
|
99
|
+
# To keep parsing simple, we use a delimiter unlikely to appear in real
|
|
100
|
+
# commit metadata: ASCII Unit Separator (\x1f).
|
|
101
|
+
COMMIT_DELIM = "\x1f"
|
|
102
|
+
|
|
103
|
+
# git log format: hash\x1fauthor\x1fdate(iso)\x1fsubject
|
|
104
|
+
# We use %x1f (the same delimiter) inline so the format string is portable.
|
|
105
|
+
# The body is fetched separately via the second invocation below.
|
|
106
|
+
FORMAT_STRING = "%H%x1f%an%x1f%ai%x1f%s"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def run_git_log(project_root, since, code_file, n=None):
|
|
110
|
+
"""Run `git log` and return a list of commit dicts.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
project_root: Path to the git repo root.
|
|
114
|
+
since: ISO date string, or None for full history.
|
|
115
|
+
code_file: Path relative to project_root to filter by.
|
|
116
|
+
n: Optional int cap on number of commits.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
List of dicts as produced by parse_commit_line + body-fetch.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
RuntimeError: if git exits non-zero or is missing.
|
|
123
|
+
"""
|
|
124
|
+
cmd = [
|
|
125
|
+
"git",
|
|
126
|
+
"-C", str(project_root),
|
|
127
|
+
"log",
|
|
128
|
+
f"--pretty=format:{FORMAT_STRING}",
|
|
129
|
+
]
|
|
130
|
+
if since:
|
|
131
|
+
cmd.append(f"--since={since}")
|
|
132
|
+
if n is not None:
|
|
133
|
+
cmd.append(f"-n{n}")
|
|
134
|
+
cmd.extend(["--", code_file])
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
proc = subprocess.run(
|
|
138
|
+
cmd,
|
|
139
|
+
capture_output=True,
|
|
140
|
+
text=True,
|
|
141
|
+
encoding="utf-8",
|
|
142
|
+
errors="replace",
|
|
143
|
+
check=False,
|
|
144
|
+
)
|
|
145
|
+
except FileNotFoundError as exc:
|
|
146
|
+
raise RuntimeError(f"git executable not found on PATH: {exc}")
|
|
147
|
+
|
|
148
|
+
if proc.returncode != 0:
|
|
149
|
+
raise RuntimeError(f"git log failed: {proc.stderr.strip()}")
|
|
150
|
+
|
|
151
|
+
commits = []
|
|
152
|
+
for line in proc.stdout.splitlines():
|
|
153
|
+
if not line:
|
|
154
|
+
continue
|
|
155
|
+
parsed = parse_commit_line(line)
|
|
156
|
+
if parsed is None:
|
|
157
|
+
continue
|
|
158
|
+
parsed["body"] = "" # filled in by fetch_body if requested later
|
|
159
|
+
commits.append(parsed)
|
|
160
|
+
return commits
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_commit_line(line):
|
|
164
|
+
"""Parse one delimited git log line. Returns dict or None on malformed input."""
|
|
165
|
+
parts = line.split(COMMIT_DELIM)
|
|
166
|
+
if len(parts) != 4:
|
|
167
|
+
return None
|
|
168
|
+
full_hash, author, date, subject = parts
|
|
169
|
+
if len(full_hash) < 7:
|
|
170
|
+
return None
|
|
171
|
+
return {
|
|
172
|
+
"hash": full_hash,
|
|
173
|
+
"short": full_hash[:7],
|
|
174
|
+
"author": author,
|
|
175
|
+
"date": date[:10], # take YYYY-MM-DD from full ISO timestamp
|
|
176
|
+
"subject": subject,
|
|
177
|
+
"body": "", # populated by fetch_commit_body
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Match PR/issue references. Order matters: longer keywords first so
|
|
182
|
+
# "Closes" doesn't get eaten by "#NNN" alone. We require word boundary
|
|
183
|
+
# (or start of string) before the keyword to avoid matching substrings
|
|
184
|
+
# like "address#N" mid-word.
|
|
185
|
+
REFS_RE = re.compile(
|
|
186
|
+
r"(?:\(|\b(?:Closes|Refs|Fixes|Resolves)\s+)"
|
|
187
|
+
r"(#\d+)",
|
|
188
|
+
re.IGNORECASE,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def extract_refs(message):
|
|
193
|
+
"""Return a list of PR/issue references found in a commit message.
|
|
194
|
+
|
|
195
|
+
Each item is either "#NNN" (from parens form) or "Keyword #NNN"
|
|
196
|
+
(from Closes/Refs/Fixes/Resolves form). Duplicates are removed
|
|
197
|
+
in order of appearance.
|
|
198
|
+
"""
|
|
199
|
+
matches = []
|
|
200
|
+
seen = set()
|
|
201
|
+
for m in REFS_RE.finditer(message):
|
|
202
|
+
prefix = m.group(0).split("#")[0]
|
|
203
|
+
ref = "#" + m.group(1)[1:] # normalize to "#NNN"
|
|
204
|
+
if ref in seen:
|
|
205
|
+
continue
|
|
206
|
+
seen.add(ref)
|
|
207
|
+
if prefix.startswith("("):
|
|
208
|
+
matches.append(ref)
|
|
209
|
+
else:
|
|
210
|
+
matches.append(f"{prefix.strip()} {ref}")
|
|
211
|
+
return matches
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def truncate_body(body, max_lines=3):
|
|
215
|
+
"""Trim a multi-line string to at most `max_lines`, stripping blank tails.
|
|
216
|
+
|
|
217
|
+
Used to keep commit bodies short in the Markdown output. The subject
|
|
218
|
+
is already shown separately; the body is supplementary context.
|
|
219
|
+
"""
|
|
220
|
+
lines = body.splitlines()
|
|
221
|
+
trimmed = lines[:max_lines]
|
|
222
|
+
while trimmed and not trimmed[-1].strip():
|
|
223
|
+
trimmed.pop()
|
|
224
|
+
return "\n".join(trimmed)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def fetch_commit_body(project_root, commit_hash):
|
|
228
|
+
"""Fetch the full commit message (subject + body) via `git show`.
|
|
229
|
+
|
|
230
|
+
Returns a string with the subject as the first line and the body
|
|
231
|
+
(if any) following a blank line. Trailing blank lines are removed.
|
|
232
|
+
"""
|
|
233
|
+
cmd = [
|
|
234
|
+
"git", "-C", str(project_root),
|
|
235
|
+
"show", "-s", "--format=%B", commit_hash,
|
|
236
|
+
]
|
|
237
|
+
try:
|
|
238
|
+
proc = subprocess.run(
|
|
239
|
+
cmd, capture_output=True, text=True,
|
|
240
|
+
encoding="utf-8", errors="replace", check=False,
|
|
241
|
+
)
|
|
242
|
+
except FileNotFoundError:
|
|
243
|
+
return ""
|
|
244
|
+
if proc.returncode != 0:
|
|
245
|
+
return ""
|
|
246
|
+
return proc.stdout.rstrip()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def render_json(meta, commits):
|
|
250
|
+
"""Render the JSON output for a `lore history` invocation.
|
|
251
|
+
|
|
252
|
+
Output matches the schema documented in the spec.
|
|
253
|
+
"""
|
|
254
|
+
payload = {
|
|
255
|
+
"entry_id": meta["entry_id"],
|
|
256
|
+
"lore_file": meta["lore_file"],
|
|
257
|
+
"code_file": meta["code_file"],
|
|
258
|
+
"since": meta["since"],
|
|
259
|
+
"since_source": meta["since_source"],
|
|
260
|
+
"commits": commits,
|
|
261
|
+
}
|
|
262
|
+
return _json.dumps(payload, indent=2, ensure_ascii=False)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def render_markdown(meta, commits):
|
|
266
|
+
"""Render the Markdown output for a `lore history` invocation.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
meta: dict with keys entry_id, lore_file, code_file, since,
|
|
270
|
+
since_source.
|
|
271
|
+
commits: list of commit dicts (see parse_commit_line + extract_refs).
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Markdown string ready for stdout.
|
|
275
|
+
"""
|
|
276
|
+
lines = []
|
|
277
|
+
lines.append(f"# history: [{meta['entry_id']}]")
|
|
278
|
+
lines.append("")
|
|
279
|
+
lines.append(f"> Entry: {meta['lore_file']}")
|
|
280
|
+
since_suffix = " (entry #added date)" if meta.get("since_source") == "entry_added" else ""
|
|
281
|
+
lines.append(f"> Since: {meta['since']}{since_suffix}")
|
|
282
|
+
lines.append(f"> File: {meta['code_file']}")
|
|
283
|
+
lines.append(f"> Commits: {len(commits)} (showing all)")
|
|
284
|
+
lines.append("")
|
|
285
|
+
|
|
286
|
+
if not commits:
|
|
287
|
+
return "\n".join(lines) + "\n"
|
|
288
|
+
|
|
289
|
+
for c in commits:
|
|
290
|
+
lines.append(f"## {c['short']} ({c['date']}, {c['author']})")
|
|
291
|
+
lines.append(c["subject"])
|
|
292
|
+
if c.get("body"):
|
|
293
|
+
body = truncate_body(c["body"], max_lines=3)
|
|
294
|
+
lines.append(f' Body: "{body}"')
|
|
295
|
+
if c.get("refs"):
|
|
296
|
+
lines.append(f" Refs: {', '.join(c['refs'])}")
|
|
297
|
+
lines.append("")
|
|
298
|
+
|
|
299
|
+
lines.append("## Suggested next step")
|
|
300
|
+
lines.append("Run `lore sync` to check whether any of these commits")
|
|
301
|
+
lines.append("introduce a [REFINED] candidate for this entry.")
|
|
302
|
+
lines.append("")
|
|
303
|
+
return "\n".join(lines)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# Exit codes per spec section "Error handling".
|
|
307
|
+
ERR_USAGE = 2 # no arg / unrecognized arg (also used by argparse path)
|
|
308
|
+
ERR_NO_LORE = 2 # .lore/ not found
|
|
309
|
+
ERR_NO_ENTRY = 3 # entry ID not in index
|
|
310
|
+
ERR_NOT_GIT = 4 # not a git repository
|
|
311
|
+
ERR_NO_GIT = 5 # git CLI missing
|
|
312
|
+
ERR_BAD_SCOPE = 6 # scope name not in scopes/
|
|
313
|
+
ERR_GIT_FAIL = 7 # git log returned non-zero for other reasons
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def die(code, message):
|
|
317
|
+
"""Print message to stderr and exit with the given code."""
|
|
318
|
+
print(f"error: {message}", file=sys.stderr)
|
|
319
|
+
sys.exit(code)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _load_entries_via_subprocess():
|
|
323
|
+
"""Run scripts/list_entries.py --json and return the parsed list.
|
|
324
|
+
|
|
325
|
+
Mirrors the pattern in find_duplicates.py / find_stale.py.
|
|
326
|
+
Returns [] if no entries.
|
|
327
|
+
"""
|
|
328
|
+
here = Path(__file__).resolve().parent
|
|
329
|
+
cmd = [sys.executable, str(here / "list_entries.py"), "--json"]
|
|
330
|
+
try:
|
|
331
|
+
proc = subprocess.run(cmd, capture_output=True, text=True,
|
|
332
|
+
encoding="utf-8", errors="replace", check=False)
|
|
333
|
+
except FileNotFoundError as exc:
|
|
334
|
+
die(ERR_NO_GIT, f"python executable not found: {exc}")
|
|
335
|
+
if proc.returncode != 0:
|
|
336
|
+
die(ERR_NO_LORE, f"list_entries.py failed: {proc.stderr.strip()}")
|
|
337
|
+
try:
|
|
338
|
+
return _json.loads(proc.stdout)
|
|
339
|
+
except _json.JSONDecodeError as exc:
|
|
340
|
+
die(ERR_NO_LORE, f"list_entries.py returned invalid JSON: {exc}")
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _find_lore_root_or_die():
|
|
344
|
+
"""Walk up from CWD to find .lore/. Die with ERR_NO_LORE if not found."""
|
|
345
|
+
p = Path(".").resolve()
|
|
346
|
+
while p != p.parent:
|
|
347
|
+
if (p / ".lore").is_dir():
|
|
348
|
+
return p
|
|
349
|
+
p = p.parent
|
|
350
|
+
die(ERR_NO_LORE, ".lore/ not found. Run 'lore init' first.")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _build_meta_entry(entry, code_file, since, since_source):
|
|
354
|
+
return {
|
|
355
|
+
"entry_id": entry["id"],
|
|
356
|
+
"lore_file": entry["file"],
|
|
357
|
+
"code_file": code_file,
|
|
358
|
+
"since": since,
|
|
359
|
+
"since_source": since_source,
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _resolve_scope_to_md_files(project_root, scope_name):
|
|
364
|
+
"""For scope form: list the (layer_file, md_path) tuples under the scope."""
|
|
365
|
+
scopes_dir = project_root / ".lore" / "scopes" / scope_name
|
|
366
|
+
if not scopes_dir.is_dir():
|
|
367
|
+
available = sorted(
|
|
368
|
+
p.name for p in (project_root / ".lore" / "scopes").iterdir()
|
|
369
|
+
if p.is_dir()
|
|
370
|
+
) if (project_root / ".lore" / "scopes").is_dir() else []
|
|
371
|
+
available_display = ", ".join(available) if available else "(none)"
|
|
372
|
+
die(ERR_BAD_SCOPE, f"Scope '{scope_name}' not found. Available: {available_display}")
|
|
373
|
+
files = []
|
|
374
|
+
for md in sorted(scopes_dir.glob("*.md")):
|
|
375
|
+
files.append((md.stem, md))
|
|
376
|
+
return files
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _is_git_repo(project_root):
|
|
380
|
+
try:
|
|
381
|
+
proc = subprocess.run(
|
|
382
|
+
["git", "-C", str(project_root), "rev-parse", "--git-dir"],
|
|
383
|
+
capture_output=True, text=True, check=False,
|
|
384
|
+
)
|
|
385
|
+
except FileNotFoundError:
|
|
386
|
+
die(ERR_NO_GIT, "git executable not found on PATH.")
|
|
387
|
+
return proc.returncode == 0
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _enrich_commits_with_body_and_refs(project_root, commits):
|
|
391
|
+
"""For each commit, fetch body and extract refs. Mutates in place."""
|
|
392
|
+
for c in commits:
|
|
393
|
+
msg = fetch_commit_body(project_root, c["hash"])
|
|
394
|
+
if msg:
|
|
395
|
+
# Body is everything after the first line.
|
|
396
|
+
parts = msg.split("\n", 1)
|
|
397
|
+
subject = parts[0]
|
|
398
|
+
body = parts[1].strip() if len(parts) > 1 else ""
|
|
399
|
+
c["subject"] = subject
|
|
400
|
+
c["body"] = truncate_body(body, max_lines=3)
|
|
401
|
+
c["refs"] = extract_refs(msg)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def main():
|
|
405
|
+
args = sys.argv[1:]
|
|
406
|
+
json_mode = "--json" in args
|
|
407
|
+
since_override = None
|
|
408
|
+
for a in args:
|
|
409
|
+
if a.startswith("--since="):
|
|
410
|
+
since_override = a.split("=", 1)[1]
|
|
411
|
+
|
|
412
|
+
positional = [a for a in args if a != "--json" and not a.startswith("--since=")]
|
|
413
|
+
if not positional:
|
|
414
|
+
print("usage: lore history <entry-id|file-path|--scope=NAME>",
|
|
415
|
+
file=sys.stderr)
|
|
416
|
+
die(ERR_USAGE, "missing argument")
|
|
417
|
+
|
|
418
|
+
parsed = parse_arg(positional[0])
|
|
419
|
+
if parsed is None:
|
|
420
|
+
die(ERR_USAGE, f"unrecognized argument: {positional[0]}")
|
|
421
|
+
|
|
422
|
+
project_root = _find_lore_root_or_die()
|
|
423
|
+
|
|
424
|
+
if not _is_git_repo(project_root):
|
|
425
|
+
die(ERR_NOT_GIT,
|
|
426
|
+
"Not a git repository. 'lore history' requires git; "
|
|
427
|
+
"use 'lore query' for in-memory answers.")
|
|
428
|
+
|
|
429
|
+
if parsed["form"] == "entry":
|
|
430
|
+
entries = _load_entries_via_subprocess()
|
|
431
|
+
entry = find_entry(entries, parsed["value"])
|
|
432
|
+
if entry is None:
|
|
433
|
+
ids = ", ".join(e["id"] for e in entries[:20])
|
|
434
|
+
more = "" if len(entries) <= 20 else f" (and {len(entries)-20} more)"
|
|
435
|
+
die(ERR_NO_ENTRY,
|
|
436
|
+
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)
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
if parsed["form"] == "file":
|
|
454
|
+
since = since_override or "1970-01-01"
|
|
455
|
+
code_file = parsed["value"]
|
|
456
|
+
try:
|
|
457
|
+
commits = run_git_log(project_root, since, code_file)
|
|
458
|
+
except RuntimeError as exc:
|
|
459
|
+
die(ERR_GIT_FAIL, str(exc))
|
|
460
|
+
_enrich_commits_with_body_and_refs(project_root, commits)
|
|
461
|
+
meta = {
|
|
462
|
+
"entry_id": f"<file:{code_file}>",
|
|
463
|
+
"lore_file": "(direct file query)",
|
|
464
|
+
"code_file": code_file,
|
|
465
|
+
"since": since,
|
|
466
|
+
"since_source": "user_arg" if since_override else "default",
|
|
467
|
+
}
|
|
468
|
+
out = render_json(meta, commits) if json_mode else render_markdown(meta, commits)
|
|
469
|
+
print(out)
|
|
470
|
+
return
|
|
471
|
+
|
|
472
|
+
if parsed["form"] == "scope":
|
|
473
|
+
layer_files = _resolve_scope_to_md_files(project_root, parsed["value"])
|
|
474
|
+
scope_payloads = [] # only used when json_mode is True
|
|
475
|
+
for layer_name, md_path in layer_files:
|
|
476
|
+
# For scope form we treat each .md file as a "code file" stand-in:
|
|
477
|
+
# we git log the md file's project-relative path to find commits
|
|
478
|
+
# that touched that lore file. (Useful for tracking lore edits.)
|
|
479
|
+
rel = str(md_path.relative_to(project_root))
|
|
480
|
+
try:
|
|
481
|
+
commits = run_git_log(project_root, "1970-01-01", rel)
|
|
482
|
+
except RuntimeError as exc:
|
|
483
|
+
die(ERR_GIT_FAIL, str(exc))
|
|
484
|
+
_enrich_commits_with_body_and_refs(project_root, commits)
|
|
485
|
+
if json_mode:
|
|
486
|
+
meta = {
|
|
487
|
+
"entry_id": f"<scope:{parsed['value']}/{layer_name}>",
|
|
488
|
+
"lore_file": rel,
|
|
489
|
+
"code_file": rel,
|
|
490
|
+
"since": "1970-01-01",
|
|
491
|
+
"since_source": "scope_form",
|
|
492
|
+
}
|
|
493
|
+
scope_payloads.append({
|
|
494
|
+
"layer": layer_name,
|
|
495
|
+
"payload": _json.loads(render_json(meta, commits)),
|
|
496
|
+
})
|
|
497
|
+
else:
|
|
498
|
+
print(f"## Scope: {parsed['value']} / {layer_name}")
|
|
499
|
+
print("")
|
|
500
|
+
if not commits:
|
|
501
|
+
print("(no commits)")
|
|
502
|
+
print("")
|
|
503
|
+
continue
|
|
504
|
+
for c in commits:
|
|
505
|
+
print(f"### {c['short']} ({c['date']}, {c['author']})")
|
|
506
|
+
print(c["subject"])
|
|
507
|
+
if c.get("body"):
|
|
508
|
+
print(f' Body: "{c["body"]}"')
|
|
509
|
+
if c.get("refs"):
|
|
510
|
+
print(f" Refs: {', '.join(c['refs'])}")
|
|
511
|
+
print("")
|
|
512
|
+
if json_mode:
|
|
513
|
+
print(_json.dumps(
|
|
514
|
+
{
|
|
515
|
+
"form": "scope",
|
|
516
|
+
"scope": parsed["value"],
|
|
517
|
+
"layers": [item["layer"] for item in scope_payloads],
|
|
518
|
+
"results": scope_payloads,
|
|
519
|
+
},
|
|
520
|
+
indent=2,
|
|
521
|
+
ensure_ascii=False,
|
|
522
|
+
))
|
|
523
|
+
return
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
if __name__ == "__main__":
|
|
527
|
+
main()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Compute the 4-char content hash for a lore entry ID.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python id_hash.py "Use Next.js App Router; reason: streaming + RSC"
|
|
6
|
+
|
|
7
|
+
Output:
|
|
8
|
+
The 4-char lowercase hex hash that goes into an entry's ID, e.g. `a3f2`.
|
|
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.
|
|
14
|
+
|
|
15
|
+
Cross-platform: works on Windows / Linux / macOS with Python 3.6+.
|
|
16
|
+
"""
|
|
17
|
+
import sys
|
|
18
|
+
import hashlib
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main():
|
|
22
|
+
if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
|
|
23
|
+
print(__doc__, file=sys.stderr)
|
|
24
|
+
sys.exit(0)
|
|
25
|
+
|
|
26
|
+
text = sys.argv[1]
|
|
27
|
+
h = hashlib.sha256(text.encode("utf-8")).hexdigest()[:4]
|
|
28
|
+
print(h)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
main()
|