lgtm-specs 0.0.4
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/.claude/settings.local.json +14 -0
- package/.gemini/README.md +8 -0
- package/.gemini/config.yaml +20 -0
- package/.gemini/styleguide.md +35 -0
- package/.github/workflows/README.md +5 -0
- package/.github/workflows/release.yml +52 -0
- package/.github/workflows/validate.yml +27 -0
- package/.prettierignore +4 -0
- package/.prettierrc +1 -0
- package/AGENTS.md +151 -0
- package/README.md +98 -0
- package/VERSION +1 -0
- package/agents/README.md +73 -0
- package/agents/modes/README.md +9 -0
- package/agents/modes/build.md +88 -0
- package/agents/modes/hack.md +76 -0
- package/agents/modes/review.md +79 -0
- package/agents/roles/builder.md +75 -0
- package/agents/roles/counsel.md +96 -0
- package/agents/roles/explorer.md +77 -0
- package/agents/roles/lead.md +76 -0
- package/agents/roles/librarian.md +63 -0
- package/agents/roles/planner.md +75 -0
- package/agents/roles/reviewer/BASE.md +9 -0
- package/agents/roles/reviewer/OUTPUT_FORMAT.md +4 -0
- package/agents/roles/reviewer/README.md +48 -0
- package/agents/roles/reviewer/lite.md +51 -0
- package/agents/roles/reviewer/logic.md +48 -0
- package/agents/roles/reviewer/performance.md +45 -0
- package/agents/roles/reviewer/plan.md +52 -0
- package/agents/roles/reviewer/quality.md +49 -0
- package/agents/roles/reviewer/security.md +47 -0
- package/agents/roles/reviewer/test.md +48 -0
- package/agents/templates/README.md +6 -0
- package/agents/templates/mode.md +33 -0
- package/agents/templates/role.md +73 -0
- package/contribute/README.md +24 -0
- package/contribute/add-agent.md +29 -0
- package/contribute/add-ci.md +31 -0
- package/contribute/add-constitution.md +17 -0
- package/contribute/add-law.md +20 -0
- package/contribute/add-policy.md +27 -0
- package/contribute/checklist.md +42 -0
- package/contribute/maintenance.md +19 -0
- package/contribute/update-models.md +47 -0
- package/docs/README.md +13 -0
- package/docs/adr/0001-knowledge-engineering-workflow.md +22 -0
- package/docs/adr/0002-rule-hierarchy.md +25 -0
- package/docs/adr/0003-atomic-knowledge-graph.md +21 -0
- package/docs/adr/0004-identification-schema.md +22 -0
- package/docs/adr/0005-agent-specialization.md +39 -0
- package/docs/adr/0006-git-workflow-integrity.md +34 -0
- package/docs/adr/0007-operating-modes-and-gates.md +54 -0
- package/docs/adr/0008-rules-vs-workflows-boundary.md +64 -0
- package/docs/adr/README.md +14 -0
- package/docs/agent_architecture.md +164 -0
- package/docs/context_lifecycle.md +228 -0
- package/docs/engineering_principles.md +128 -0
- package/docs/local_policies.md +59 -0
- package/docs/meta/collaborative_dynamics.md +142 -0
- package/docs/meta/domains/README.md +8 -0
- package/docs/meta/domains/bitcoin/01-units.md +21 -0
- package/docs/meta/domains/bitcoin/02-broadcast-cancellation.md +20 -0
- package/docs/meta/domains/bitcoin/03-fee-rates-rounding.md +21 -0
- package/docs/meta/domains/bitcoin/04-confirmations-reorgs.md +20 -0
- package/docs/meta/domains/bitcoin/05-address-gap-limit.md +16 -0
- package/docs/meta/domains/bitcoin/06-relay-policy.md +27 -0
- package/docs/meta/domains/bitcoin/README.md +12 -0
- package/docs/meta/domains/git/01-workflow.md +89 -0
- package/docs/meta/domains/git/02-commits.md +57 -0
- package/docs/meta/domains/git/03-collaboration.md +40 -0
- package/docs/meta/domains/git/04-integrity.md +26 -0
- package/docs/meta/domains/git/05-configuration.md +209 -0
- package/docs/meta/domains/git/06-advanced.md +130 -0
- package/docs/meta/domains/git/README.md +29 -0
- package/docs/meta/industry_best_practices.md +555 -0
- package/docs/meta/languages/README.md +8 -0
- package/docs/meta/languages/go/01-concurrency.md +37 -0
- package/docs/meta/languages/go/02-api-design.md +30 -0
- package/docs/meta/languages/go/03-resilience.md +27 -0
- package/docs/meta/languages/go/04-errors.md +27 -0
- package/docs/meta/languages/go/05-performance.md +18 -0
- package/docs/meta/languages/go/06-safety.md +18 -0
- package/docs/meta/languages/go/07-testing.md +44 -0
- package/docs/meta/languages/go/08-config-layout.md +23 -0
- package/docs/meta/languages/go/README.md +14 -0
- package/docs/meta/languages/typescript/01-strictness.md +19 -0
- package/docs/meta/languages/typescript/02-immutability.md +15 -0
- package/docs/meta/languages/typescript/03-async.md +18 -0
- package/docs/meta/languages/typescript/04-design.md +19 -0
- package/docs/meta/languages/typescript/05-control-flow.md +11 -0
- package/docs/meta/languages/typescript/README.md +11 -0
- package/docs/meta/workflow.md +68 -0
- package/docs/philosophy.md +36 -0
- package/integrate/README.md +459 -0
- package/integrate/versioning.md +41 -0
- package/models/README.md +68 -0
- package/models/registry.yaml +55 -0
- package/package.json +11 -0
- package/rules/README.md +57 -0
- package/rules/RULE-00000-EXAMPLE.md +29 -0
- package/rules/constitution/CONS-00001-srp.md +40 -0
- package/rules/constitution/CONS-00002-ocp.md +43 -0
- package/rules/constitution/CONS-00003-lsp.md +44 -0
- package/rules/constitution/CONS-00004-isp.md +46 -0
- package/rules/constitution/CONS-00005-dip.md +37 -0
- package/rules/constitution/CONS-00006-dry.md +45 -0
- package/rules/constitution/CONS-00007-demeter.md +35 -0
- package/rules/constitution/CONS-00008-composition.md +44 -0
- package/rules/constitution/CONS-00009-deep-modules.md +39 -0
- package/rules/constitution/CONS-00010-kiss.md +47 -0
- package/rules/constitution/CONS-00011-yagni.md +49 -0
- package/rules/constitution/CONS-00012-cognitive-limits.md +28 -0
- package/rules/constitution/CONS-00013-boy-scout.md +27 -0
- package/rules/constitution/CONS-00014-broken-windows.md +35 -0
- package/rules/constitution/CONS-00015-safety.md +46 -0
- package/rules/constitution/CONS-00016-cqs.md +39 -0
- package/rules/constitution/CONS-00017-postel.md +35 -0
- package/rules/constitution/CONS-00018-cap.md +35 -0
- package/rules/constitution/CONS-00019-fallacies.md +37 -0
- package/rules/constitution/CONS-00020-shift-left.md +28 -0
- package/rules/constitution/CONS-00021-congruence.md +28 -0
- package/rules/constitution/CONS-00022-orthogonality.md +40 -0
- package/rules/constitution/CONS-00023-determinism.md +38 -0
- package/rules/constitution/CONS-00024-security.md +42 -0
- package/rules/constitution/CONS-00025-efficiency.md +38 -0
- package/rules/constitution/CONS-00026-resilience.md +41 -0
- package/rules/constitution/CONS-00027-transparency.md +40 -0
- package/rules/constitution/CONS-00028-evolvability.md +36 -0
- package/rules/constitution/CONS-00029-operability.md +36 -0
- package/rules/constitution/CONS-00030-rework-cycle.md +27 -0
- package/rules/constitution/CONS-00031-checklist.md +28 -0
- package/rules/constitution/CONS-00032-documentation.md +39 -0
- package/rules/constitution/README.md +52 -0
- package/rules/laws/README.md +15 -0
- package/rules/laws/bitcoin/BTC-00001-amounts-as-satoshis.md +39 -0
- package/rules/laws/bitcoin/BTC-00002-broadcast-not-cancelable.md +36 -0
- package/rules/laws/bitcoin/BTC-00003-fee-rate-math-rounding.md +37 -0
- package/rules/laws/bitcoin/BTC-00004-confirmations-and-reorgs.md +40 -0
- package/rules/laws/bitcoin/BTC-00005-address-gap-limit.md +37 -0
- package/rules/laws/bitcoin/BTC-00006-relay-is-policy-dependent.md +36 -0
- package/rules/laws/bitcoin/BTC-00007-dust-policy.md +36 -0
- package/rules/laws/bitcoin/BTC-00008-min-relay-fee.md +36 -0
- package/rules/laws/bitcoin/BTC-00009-feefilter.md +36 -0
- package/rules/laws/bitcoin/README.md +29 -0
- package/rules/laws/default.md +30 -0
- package/rules/laws/git/GIT-00001-atomic-commit.md +29 -0
- package/rules/laws/git/GIT-00002-imperative-subject.md +27 -0
- package/rules/laws/git/GIT-00003-formatting-50-72.md +28 -0
- package/rules/laws/git/GIT-00004-trunk-based.md +28 -0
- package/rules/laws/git/GIT-00005-public-immutability.md +26 -0
- package/rules/laws/git/GIT-00006-signing.md +27 -0
- package/rules/laws/git/GIT-00007-reviewer-capital.md +26 -0
- package/rules/laws/git/GIT-00008-patch-series.md +28 -0
- package/rules/laws/git/GIT-00009-branch-naming.md +28 -0
- package/rules/laws/git/GIT-00010-pr-hygiene.md +51 -0
- package/rules/laws/git/GIT-00011-merge-method.md +35 -0
- package/rules/laws/git/GIT-00012-conflict-resolution.md +35 -0
- package/rules/laws/git/GIT-00013-ignore-standards.md +38 -0
- package/rules/laws/git/GIT-00014-lfs-large-binaries.md +37 -0
- package/rules/laws/git/GIT-00015-git-hooks.md +35 -0
- package/rules/laws/git/GIT-00016-branch-protection.md +34 -0
- package/rules/laws/git/GIT-00017-secrets-management.md +34 -0
- package/rules/laws/git/GIT-00018-ci-enforcement.md +33 -0
- package/rules/laws/git/GIT-00019-review-checklist.md +39 -0
- package/rules/laws/git/GIT-00020-issue-references.md +34 -0
- package/rules/laws/git/GIT-00021-partial-staging.md +38 -0
- package/rules/laws/git/GIT-00022-feature-flags.md +33 -0
- package/rules/laws/git/GIT-00023-breaking-changes.md +41 -0
- package/rules/laws/git/GIT-00024-dependency-management.md +44 -0
- package/rules/laws/git/GIT-00025-large-repository-optimization.md +54 -0
- package/rules/laws/git/README.md +31 -0
- package/rules/laws/go/GO-00001-actor-model.md +51 -0
- package/rules/laws/go/GO-00002-api-design.md +37 -0
- package/rules/laws/go/GO-00003-error-handling.md +43 -0
- package/rules/laws/go/GO-00004-context.md +45 -0
- package/rules/laws/go/GO-00005-performance.md +40 -0
- package/rules/laws/go/GO-00006-packages.md +29 -0
- package/rules/laws/go/GO-00007-circuit-breakers.md +43 -0
- package/rules/laws/go/GO-00008-safety.md +39 -0
- package/rules/laws/go/GO-00009-table-driven-test.md +48 -0
- package/rules/laws/go/GO-00010-escape-analysis.md +37 -0
- package/rules/laws/go/GO-00011-retry.md +45 -0
- package/rules/laws/go/GO-00012-rate-limiting.md +42 -0
- package/rules/laws/go/GO-00013-io-buffering.md +43 -0
- package/rules/laws/go/GO-00014-memory-layout.md +41 -0
- package/rules/laws/go/GO-00015-aaa-pattern.md +49 -0
- package/rules/laws/go/GO-00016-test-libraries.md +35 -0
- package/rules/laws/go/GO-00017-comments.md +37 -0
- package/rules/laws/go/GO-00018-test-isolation.md +38 -0
- package/rules/laws/go/GO-00019-test-comments.md +36 -0
- package/rules/laws/go/GO-00020-mocking.md +36 -0
- package/rules/laws/go/GO-00021-configuration.md +36 -0
- package/rules/laws/go/GO-00022-observability.md +34 -0
- package/rules/laws/go/GO-00023-dependency-management.md +28 -0
- package/rules/laws/go/GO-00024-project-layout.md +30 -0
- package/rules/laws/go/GO-00025-concurrency-patterns.md +39 -0
- package/rules/laws/go/README.md +45 -0
- package/rules/laws/typescript/README.md +14 -0
- package/rules/laws/typescript/TS-00001-no-any.md +39 -0
- package/rules/laws/typescript/TS-00002-immutability.md +36 -0
- package/rules/laws/typescript/TS-00003-async.md +35 -0
- package/rules/laws/typescript/TS-00004-strict-null.md +38 -0
- package/rules/laws/typescript/TS-00005-unions.md +35 -0
- package/rules/laws/typescript/TS-00006-interface.md +38 -0
- package/rules/laws/typescript/TS-00007-generics.md +38 -0
- package/rules/laws/typescript/TS-00008-modules.md +28 -0
- package/rules/policies/README.md +12 -0
- package/rules/policies/default.md +28 -0
- package/scripts/README.md +45 -0
- package/scripts/generate_release_notes.py +376 -0
- package/scripts/validate_specs.py +730 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Module Organization (TS-00008)
|
|
2
|
+
|
|
3
|
+
**Source**: [The Law of TypeScript](../../../docs/meta/languages/typescript/04-design.md#3-module-organization)
|
|
4
|
+
**Tags**: #structural #typescript #architecture
|
|
5
|
+
**Related**: [Clean Packages](../go/GO-00006-packages.md)
|
|
6
|
+
|
|
7
|
+
## Definition
|
|
8
|
+
|
|
9
|
+
Organize code by Feature/Domain, not by technical type.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
1. **Feature Folders**: Group related components, styles, and tests in one folder.
|
|
14
|
+
2. **No `types/`**: Keep types co-located with the code that uses them.
|
|
15
|
+
3. **Barrels**: Use `index.ts` to export public API of a module.
|
|
16
|
+
|
|
17
|
+
## Anti-Patterns
|
|
18
|
+
|
|
19
|
+
- **Technical Split**: `src/components`, `src/utils`, `src/types` (MVC style).
|
|
20
|
+
- **Deep Imports**: Importing `module/internal/helper` instead of `module`.
|
|
21
|
+
|
|
22
|
+
## Examples
|
|
23
|
+
|
|
24
|
+
**Bad:**
|
|
25
|
+
`src/types/User.ts`, `src/controllers/UserController.ts`
|
|
26
|
+
|
|
27
|
+
**Good:**
|
|
28
|
+
`src/features/user/types.ts`, `src/features/user/controller.ts`
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Project Policies
|
|
2
|
+
|
|
3
|
+
This directory contains repository-specific configuration and style rules.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
- **Default**: `default.md` is used if no other policy matches.
|
|
8
|
+
- **Custom**: Add `[project]-policy.md` for specific repos.
|
|
9
|
+
|
|
10
|
+
## Index
|
|
11
|
+
|
|
12
|
+
- [Default Policy](default.md)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Default Policy (POL-00000)
|
|
2
|
+
|
|
3
|
+
**Source**: [Industry Best Practices](../../docs/meta/industry_best_practices.md#1-solid-principles)
|
|
4
|
+
**Tags**: #operational #policy #default
|
|
5
|
+
**Related**: [KISS](../constitution/CONS-00010-kiss.md)
|
|
6
|
+
|
|
7
|
+
## Definition
|
|
8
|
+
|
|
9
|
+
Baseline defaults for any project without specific configuration.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
1. **Format**: Use standard language formatters (`gofmt`, `prettier`).
|
|
14
|
+
2. **Lint**: Use standard linters (`golangci-lint`, `eslint`).
|
|
15
|
+
3. **Naming**: Idiomatic casing (camelCase for JS, snake_case for Python).
|
|
16
|
+
|
|
17
|
+
## Anti-Patterns
|
|
18
|
+
|
|
19
|
+
- **Custom Style**: Arguing about whitespace.
|
|
20
|
+
- **No CI**: Merging without checks.
|
|
21
|
+
|
|
22
|
+
## Examples
|
|
23
|
+
|
|
24
|
+
**Bad:**
|
|
25
|
+
Manually formatting JSON.
|
|
26
|
+
|
|
27
|
+
**Good:**
|
|
28
|
+
`bun run format`
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Scripts
|
|
2
|
+
|
|
3
|
+
## Index
|
|
4
|
+
|
|
5
|
+
- `validate_specs.py`: Repository integrity checks (rules structure, indexing, capability registry).
|
|
6
|
+
- `generate_release_notes.py`: Categorized release notes for GitHub releases.
|
|
7
|
+
|
|
8
|
+
## Validation
|
|
9
|
+
|
|
10
|
+
Run validation:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
python3 scripts/validate_specs.py
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Optional: validate external Markdown links (network-dependent):
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
python3 scripts/validate_specs.py --check-urls
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Tune timeout if needed:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python3 scripts/validate_specs.py --check-urls --url-timeout 12
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Ignore a specific external URL (repeatable):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python3 scripts/validate_specs.py --check-urls --url-ignore "https://example.com/badge.svg"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Run the full local check (validation + formatting):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bun run check
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If the repo ever needs a temporary warnings baseline during a migration:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python3 scripts/validate_specs.py --write-baseline scripts/validate_baseline.txt
|
|
44
|
+
python3 scripts/validate_specs.py --baseline scripts/validate_baseline.txt
|
|
45
|
+
```
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Generate GitHub release notes for a semver tag.
|
|
2
|
+
|
|
3
|
+
This script is intended to run in CI on tag push.
|
|
4
|
+
|
|
5
|
+
Output goals:
|
|
6
|
+
- Summary paragraph.
|
|
7
|
+
- Highlights grouped by impacted repo area (Knowledge, Agents, Modes, Integration, Models, etc.).
|
|
8
|
+
- Keep it scannable; omit long commit/PR listings.
|
|
9
|
+
|
|
10
|
+
Inputs:
|
|
11
|
+
- Tag name like "v0.0.2" (from GITHUB_REF_NAME).
|
|
12
|
+
- Git history with tags available (checkout fetch-depth 0).
|
|
13
|
+
|
|
14
|
+
Auth:
|
|
15
|
+
- If GITHUB_TOKEN is set, the script fetches PR metadata and file lists via GitHub API.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
import urllib.parse
|
|
27
|
+
import urllib.request
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
TYPE_RE = re.compile(r"^(?P<type>[a-z]+)(\((?P<scope>[^)]+)\))?:\s+(?P<rest>.+)$")
|
|
32
|
+
MERGE_PR_RE = re.compile(r"^Merge pull request #(?P<num>\d+)\b")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class PR:
|
|
37
|
+
number: int
|
|
38
|
+
title: str
|
|
39
|
+
url: str
|
|
40
|
+
type: str
|
|
41
|
+
keyword: str
|
|
42
|
+
files: tuple[str, ...] = ()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def sh(*args: str) -> str:
|
|
46
|
+
out = subprocess.check_output(list(args), stderr=subprocess.STDOUT)
|
|
47
|
+
return out.decode("utf-8").strip()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def git_tags() -> list[str]:
|
|
51
|
+
txt = sh("git", "tag", "-l", "v*", "--sort=-version:refname")
|
|
52
|
+
return [line.strip() for line in txt.splitlines() if line.strip()]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def previous_tag(current: str) -> str | None:
|
|
56
|
+
for t in git_tags():
|
|
57
|
+
if t != current:
|
|
58
|
+
return t
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def merged_pr_merges(prev: str | None, cur: str) -> list[tuple[int, str]]:
|
|
63
|
+
"""Return (pr_number, merge_commit_sha) pairs."""
|
|
64
|
+
|
|
65
|
+
rev_range = f"{prev}..{cur}" if prev else cur
|
|
66
|
+
txt = sh(
|
|
67
|
+
"git",
|
|
68
|
+
"log",
|
|
69
|
+
"--merges",
|
|
70
|
+
"--first-parent",
|
|
71
|
+
"--pretty=%H %s",
|
|
72
|
+
rev_range,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
out: list[tuple[int, str]] = []
|
|
76
|
+
seen: set[int] = set()
|
|
77
|
+
for line in txt.splitlines():
|
|
78
|
+
line = line.strip()
|
|
79
|
+
if not line:
|
|
80
|
+
continue
|
|
81
|
+
sha, subj = line.split(" ", 1)
|
|
82
|
+
m = MERGE_PR_RE.match(subj.strip())
|
|
83
|
+
if not m:
|
|
84
|
+
continue
|
|
85
|
+
n = int(m.group("num"))
|
|
86
|
+
if n in seen:
|
|
87
|
+
continue
|
|
88
|
+
seen.add(n)
|
|
89
|
+
out.append((n, sha))
|
|
90
|
+
return out
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def merge_pr_title_from_commit(sha: str) -> str | None:
|
|
94
|
+
"""Extract PR title from a GitHub merge commit body."""
|
|
95
|
+
|
|
96
|
+
body = sh("git", "log", "-1", "--format=%b", sha)
|
|
97
|
+
for line in body.splitlines():
|
|
98
|
+
t = line.strip()
|
|
99
|
+
if t:
|
|
100
|
+
return t
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def files_from_commit(sha: str) -> tuple[str, ...]:
|
|
105
|
+
# Merge commits require `-m` to surface per-parent file lists.
|
|
106
|
+
txt = sh("git", "diff-tree", "-m", "--no-commit-id", "--name-only", "-r", sha)
|
|
107
|
+
files = [line.strip() for line in txt.splitlines() if line.strip()]
|
|
108
|
+
return tuple(sorted({f for f in files if f}))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def local_pr(repo: str, num: int, sha: str) -> PR:
|
|
112
|
+
url = f"https://github.com/{repo}/pull/{num}"
|
|
113
|
+
title = merge_pr_title_from_commit(sha) or f"PR #{num}"
|
|
114
|
+
return PR(
|
|
115
|
+
number=num,
|
|
116
|
+
title=title,
|
|
117
|
+
url=url,
|
|
118
|
+
type=infer_type_from_title(title),
|
|
119
|
+
keyword=infer_keyword_from_title(title),
|
|
120
|
+
files=files_from_commit(sha),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def infer_type_from_title(title: str) -> str:
|
|
125
|
+
m = TYPE_RE.match(title.strip())
|
|
126
|
+
if not m:
|
|
127
|
+
return "other"
|
|
128
|
+
t = m.group("type")
|
|
129
|
+
return t.lower()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def infer_keyword_from_title(title: str) -> str:
|
|
133
|
+
m = TYPE_RE.match(title.strip())
|
|
134
|
+
if not m:
|
|
135
|
+
return "other"
|
|
136
|
+
scope = m.group("scope")
|
|
137
|
+
t = m.group("type")
|
|
138
|
+
return (scope or t or "other").lower()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def strip_type_prefix(title: str) -> str:
|
|
142
|
+
m = TYPE_RE.match(title.strip())
|
|
143
|
+
return m.group("rest") if m else title.strip()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def github_api_get_json(url: str, token: str | None) -> dict:
|
|
147
|
+
req = urllib.request.Request(url)
|
|
148
|
+
req.add_header("User-Agent", "lgtm-specs-release-notes")
|
|
149
|
+
req.add_header("Accept", "application/vnd.github+json")
|
|
150
|
+
if token:
|
|
151
|
+
req.add_header("Authorization", f"Bearer {token}")
|
|
152
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
153
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def fetch_pr_files(repo: str, num: int, token: str | None) -> tuple[str, ...]:
|
|
157
|
+
if not token:
|
|
158
|
+
return ()
|
|
159
|
+
|
|
160
|
+
files: list[str] = []
|
|
161
|
+
page = 1
|
|
162
|
+
per_page = 100
|
|
163
|
+
while True:
|
|
164
|
+
api = f"https://api.github.com/repos/{repo}/pulls/{num}/files?per_page={per_page}&page={page}"
|
|
165
|
+
data = github_api_get_json(api, token)
|
|
166
|
+
if not isinstance(data, list):
|
|
167
|
+
break
|
|
168
|
+
batch = [str(item.get("filename")) for item in data if isinstance(item, dict) and item.get("filename")]
|
|
169
|
+
files.extend(batch)
|
|
170
|
+
if len(batch) < per_page:
|
|
171
|
+
break
|
|
172
|
+
page += 1
|
|
173
|
+
|
|
174
|
+
# Stable ordering.
|
|
175
|
+
return tuple(sorted({f for f in files if f}))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def fetch_pr(repo: str, num: int, token: str | None) -> PR:
|
|
179
|
+
api = f"https://api.github.com/repos/{repo}/pulls/{num}"
|
|
180
|
+
data = github_api_get_json(api, token)
|
|
181
|
+
title = str(data.get("title") or f"PR #{num}")
|
|
182
|
+
url = str(data.get("html_url") or f"https://github.com/{repo}/pull/{num}")
|
|
183
|
+
return PR(
|
|
184
|
+
number=num,
|
|
185
|
+
title=title,
|
|
186
|
+
url=url,
|
|
187
|
+
type=infer_type_from_title(title),
|
|
188
|
+
keyword=infer_keyword_from_title(title),
|
|
189
|
+
files=fetch_pr_files(repo, num, token),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def pr_areas(pr: PR) -> set[str]:
|
|
194
|
+
# Repo-area classification for highlights.
|
|
195
|
+
areas: set[str] = set()
|
|
196
|
+
|
|
197
|
+
for f in pr.files:
|
|
198
|
+
if f.startswith("rules/") or f.startswith("docs/meta/"):
|
|
199
|
+
areas.add("Knowledge")
|
|
200
|
+
if f.startswith("agents/modes/"):
|
|
201
|
+
areas.add("Modes")
|
|
202
|
+
elif f.startswith("agents/roles/"):
|
|
203
|
+
areas.add("Agents")
|
|
204
|
+
elif f.startswith("agents/"):
|
|
205
|
+
areas.add("Agents")
|
|
206
|
+
if f.startswith("integrate/"):
|
|
207
|
+
areas.add("Integration")
|
|
208
|
+
if f.startswith("models/"):
|
|
209
|
+
areas.add("Models")
|
|
210
|
+
if f.startswith(".github/workflows/"):
|
|
211
|
+
areas.add("CI")
|
|
212
|
+
if f.startswith("scripts/"):
|
|
213
|
+
areas.add("Tooling")
|
|
214
|
+
if f.startswith("contribute/"):
|
|
215
|
+
areas.add("Contributing")
|
|
216
|
+
|
|
217
|
+
if not areas:
|
|
218
|
+
# Fallback: infer from conventional scope/type.
|
|
219
|
+
kw = (pr.keyword or pr.type or "").lower()
|
|
220
|
+
if kw in ("agents", "review", "reviewers"):
|
|
221
|
+
areas.add("Agents")
|
|
222
|
+
elif kw in ("integrate", "integration"):
|
|
223
|
+
areas.add("Integration")
|
|
224
|
+
elif kw in ("models", "model"):
|
|
225
|
+
areas.add("Models")
|
|
226
|
+
elif kw in ("ci", "release"):
|
|
227
|
+
areas.add("CI")
|
|
228
|
+
elif kw in ("docs",):
|
|
229
|
+
areas.add("Knowledge")
|
|
230
|
+
else:
|
|
231
|
+
areas.add("Other")
|
|
232
|
+
|
|
233
|
+
return areas
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def keyword_search_url(repo: str, keyword: str) -> str:
|
|
237
|
+
# Search merged PR titles for a keyword or conventional prefix.
|
|
238
|
+
conventional = {
|
|
239
|
+
"feat",
|
|
240
|
+
"fix",
|
|
241
|
+
"docs",
|
|
242
|
+
"ci",
|
|
243
|
+
"chore",
|
|
244
|
+
"refactor",
|
|
245
|
+
"test",
|
|
246
|
+
"perf",
|
|
247
|
+
"build",
|
|
248
|
+
"style",
|
|
249
|
+
}
|
|
250
|
+
needle = f"{keyword}:" if keyword in conventional else keyword
|
|
251
|
+
q = f"is:pr is:merged in:title \"{needle}\""
|
|
252
|
+
return f"https://github.com/{repo}/pulls?q={urllib.parse.quote(q)}"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def pr_link(pr: PR) -> str:
|
|
256
|
+
return f"[#${pr.number}]({pr.url})".replace("#$", "#")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def summary_paragraph(tag: str, prev: str | None, prs: list[PR], areas_sorted: list[str]) -> str:
|
|
260
|
+
n = len(prs)
|
|
261
|
+
prev_txt = prev if prev else "the previous release"
|
|
262
|
+
if not areas_sorted or areas_sorted == ["Other"]:
|
|
263
|
+
return f"{tag} includes {n} merged PR(s) since {prev_txt}."
|
|
264
|
+
|
|
265
|
+
top = areas_sorted[:3]
|
|
266
|
+
if len(top) == 1:
|
|
267
|
+
focus = top[0]
|
|
268
|
+
elif len(top) == 2:
|
|
269
|
+
focus = f"{top[0]} and {top[1]}"
|
|
270
|
+
else:
|
|
271
|
+
focus = f"{top[0]}, {top[1]}, and {top[2]}"
|
|
272
|
+
|
|
273
|
+
return f"{tag} includes {n} merged PR(s) since {prev_txt}, focusing on {focus}."
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def compare_url(repo: str, prev: str | None, cur: str) -> str:
|
|
277
|
+
if not prev:
|
|
278
|
+
return f"https://github.com/{repo}/commits/{cur}"
|
|
279
|
+
return f"https://github.com/{repo}/compare/{prev}...{cur}"
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def main() -> int:
|
|
283
|
+
ap = argparse.ArgumentParser(add_help=True)
|
|
284
|
+
ap.add_argument("--tag", type=str, default=os.environ.get("GITHUB_REF_NAME"))
|
|
285
|
+
ap.add_argument("--repo", type=str, default=os.environ.get("GITHUB_REPOSITORY"))
|
|
286
|
+
ap.add_argument("--output", type=str, required=True)
|
|
287
|
+
args = ap.parse_args()
|
|
288
|
+
|
|
289
|
+
tag = (args.tag or "").strip()
|
|
290
|
+
repo = (args.repo or "").strip()
|
|
291
|
+
if not tag:
|
|
292
|
+
print("ERROR: missing --tag (or GITHUB_REF_NAME)", file=sys.stderr)
|
|
293
|
+
return 2
|
|
294
|
+
if not repo:
|
|
295
|
+
print("ERROR: missing --repo (or GITHUB_REPOSITORY)", file=sys.stderr)
|
|
296
|
+
return 2
|
|
297
|
+
|
|
298
|
+
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
|
299
|
+
prev = previous_tag(tag)
|
|
300
|
+
pr_merges = merged_pr_merges(prev, tag)
|
|
301
|
+
|
|
302
|
+
prs: list[PR] = []
|
|
303
|
+
for n, sha in pr_merges:
|
|
304
|
+
if token:
|
|
305
|
+
try:
|
|
306
|
+
prs.append(fetch_pr(repo, n, token))
|
|
307
|
+
continue
|
|
308
|
+
except Exception:
|
|
309
|
+
pass
|
|
310
|
+
prs.append(local_pr(repo, n, sha))
|
|
311
|
+
|
|
312
|
+
# Group by impacted area.
|
|
313
|
+
by_area: dict[str, list[PR]] = {}
|
|
314
|
+
for pr in prs:
|
|
315
|
+
for area in pr_areas(pr):
|
|
316
|
+
by_area.setdefault(area, []).append(pr)
|
|
317
|
+
|
|
318
|
+
area_order = [
|
|
319
|
+
"Knowledge",
|
|
320
|
+
"Agents",
|
|
321
|
+
"Modes",
|
|
322
|
+
"Integration",
|
|
323
|
+
"Models",
|
|
324
|
+
"CI",
|
|
325
|
+
"Tooling",
|
|
326
|
+
"Contributing",
|
|
327
|
+
"Other",
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
areas_sorted = [a for a in area_order if a in by_area]
|
|
331
|
+
|
|
332
|
+
lines: list[str] = []
|
|
333
|
+
lines.append(f"# {tag}")
|
|
334
|
+
lines.append("")
|
|
335
|
+
lines.append("## Summary")
|
|
336
|
+
lines.append("")
|
|
337
|
+
lines.append(summary_paragraph(tag, prev, prs, [a for a in areas_sorted if a != "Other"]))
|
|
338
|
+
lines.append("")
|
|
339
|
+
|
|
340
|
+
if prs:
|
|
341
|
+
lines.append("## Highlights")
|
|
342
|
+
lines.append("")
|
|
343
|
+
|
|
344
|
+
# Show up to 6 areas. If everything is "Other", show it anyway.
|
|
345
|
+
areas_to_show = [a for a in areas_sorted if a != "Other"]
|
|
346
|
+
if not areas_to_show and "Other" in areas_sorted:
|
|
347
|
+
areas_to_show = ["Other"]
|
|
348
|
+
|
|
349
|
+
for area in areas_to_show[:6]:
|
|
350
|
+
items = by_area.get(area, [])
|
|
351
|
+
if not items:
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
# Prefer up to 2 representative PR titles to describe the area.
|
|
355
|
+
reps = items[:2]
|
|
356
|
+
titles = "; ".join(strip_type_prefix(p.title) for p in reps)
|
|
357
|
+
links = ", ".join(pr_link(p) for p in items[:4])
|
|
358
|
+
if len(items) > 4:
|
|
359
|
+
links = f"{links}, ..."
|
|
360
|
+
|
|
361
|
+
lines.append(f"- {area}: {titles} ({links})")
|
|
362
|
+
|
|
363
|
+
lines.append("")
|
|
364
|
+
|
|
365
|
+
lines.append(f"**Full Changelog**: {compare_url(repo, prev, tag)}")
|
|
366
|
+
lines.append("")
|
|
367
|
+
|
|
368
|
+
out = "\n".join(lines)
|
|
369
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
370
|
+
f.write(out)
|
|
371
|
+
|
|
372
|
+
return 0
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
if __name__ == "__main__":
|
|
376
|
+
raise SystemExit(main())
|