willow-reconciler 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. willow_reconciler-0.2.0/.github/FUNDING.yml +1 -0
  2. willow_reconciler-0.2.0/.github/ISSUE_TEMPLATE/bug_report.md +23 -0
  3. willow_reconciler-0.2.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
  4. willow_reconciler-0.2.0/.github/ISSUE_TEMPLATE/feature_request.md +18 -0
  5. willow_reconciler-0.2.0/.github/pull_request_template.md +31 -0
  6. willow_reconciler-0.2.0/.github/workflows/release-please.yml +64 -0
  7. willow_reconciler-0.2.0/.github/workflows/release.yml +68 -0
  8. willow_reconciler-0.2.0/.github/workflows/tests.yml +39 -0
  9. willow_reconciler-0.2.0/.github/workflows/trailers.yml +41 -0
  10. willow_reconciler-0.2.0/.gitignore +5 -0
  11. willow_reconciler-0.2.0/.release-please-manifest.json +3 -0
  12. willow_reconciler-0.2.0/CHANGELOG.md +35 -0
  13. willow_reconciler-0.2.0/CODE_OF_CONDUCT.md +39 -0
  14. willow_reconciler-0.2.0/CONTRIBUTING.md +44 -0
  15. willow_reconciler-0.2.0/CONVENTION.md +85 -0
  16. willow_reconciler-0.2.0/LICENSE +202 -0
  17. willow_reconciler-0.2.0/PKG-INFO +17 -0
  18. willow_reconciler-0.2.0/README.md +125 -0
  19. willow_reconciler-0.2.0/SECURITY.md +35 -0
  20. willow_reconciler-0.2.0/SUPPORT.md +10 -0
  21. willow_reconciler-0.2.0/docs/ideas.md +77 -0
  22. willow_reconciler-0.2.0/pyproject.toml +33 -0
  23. willow_reconciler-0.2.0/reconciler/__init__.py +18 -0
  24. willow_reconciler-0.2.0/reconciler/classify.py +167 -0
  25. willow_reconciler-0.2.0/reconciler/cli.py +265 -0
  26. willow_reconciler-0.2.0/reconciler/emit.py +51 -0
  27. willow_reconciler-0.2.0/reconciler/gitevidence.py +213 -0
  28. willow_reconciler-0.2.0/reconciler/hooks.py +195 -0
  29. willow_reconciler-0.2.0/reconciler/ids.py +17 -0
  30. willow_reconciler-0.2.0/reconciler/ledger.py +102 -0
  31. willow_reconciler-0.2.0/reconciler/parse.py +51 -0
  32. willow_reconciler-0.2.0/reconciler/validate.py +119 -0
  33. willow_reconciler-0.2.0/reconciler/verify.py +75 -0
  34. willow_reconciler-0.2.0/release-please-config.json +26 -0
  35. willow_reconciler-0.2.0/tests/test_classify.py +216 -0
  36. willow_reconciler-0.2.0/tests/test_cli.py +130 -0
  37. willow_reconciler-0.2.0/tests/test_emit.py +56 -0
  38. willow_reconciler-0.2.0/tests/test_gitevidence.py +182 -0
  39. willow_reconciler-0.2.0/tests/test_hooks.py +199 -0
  40. willow_reconciler-0.2.0/tests/test_ids.py +18 -0
  41. willow_reconciler-0.2.0/tests/test_ledger.py +65 -0
  42. willow_reconciler-0.2.0/tests/test_parse.py +60 -0
  43. willow_reconciler-0.2.0/tests/test_validation.py +110 -0
  44. willow_reconciler-0.2.0/tests/test_verify.py +82 -0
@@ -0,0 +1 @@
1
+ github: [rudi193-cmd]
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: Bug report
3
+ about: Something does the wrong thing
4
+ labels: bug
5
+ ---
6
+
7
+ ## What happened
8
+
9
+ <!-- The wrong behavior, plainly. -->
10
+
11
+ ## What should have happened
12
+
13
+ <!-- The right behavior, plainly. -->
14
+
15
+ ## Receipts
16
+
17
+ <!-- How to reproduce: exact commands, versions, OS. Paste real
18
+ output, not a paraphrase. If data/privacy is involved, redact
19
+ values but keep the shape. -->
20
+
21
+ ## Blast radius
22
+
23
+ <!-- What it breaks, who hits it, any workaround you found. -->
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: Security report
4
+ url: https://github.com/willow-memory/willow-reconciler/security/advisories/new
5
+ about: Exploitable findings go here privately — never in a public issue.
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: Feature request
3
+ about: Something this tool should do
4
+ labels: enhancement
5
+ ---
6
+
7
+ ## Bite
8
+
9
+ <!-- One sentence — the single outcome you want. -->
10
+
11
+ ## Why
12
+
13
+ <!-- The gap or friction that makes this worth building. A real
14
+ scenario beats an abstraction. -->
15
+
16
+ ## Out of scope
17
+
18
+ <!-- What this deliberately is NOT asking for, if you can see the line. -->
@@ -0,0 +1,31 @@
1
+ ## Bite
2
+
3
+ <!-- One sentence — the single outcome this PR delivers.
4
+ Follow-up to a merged PR? Say so: "Follow-up to #NN (merged)". -->
5
+
6
+ ## What was done
7
+
8
+ <!-- Bullets, boldest fact first. If two unrelated changes rode this
9
+ branch, flag them separately so they can be reviewed (or split) cleanly. -->
10
+
11
+ ## Evidence
12
+
13
+ <!-- Receipts, not claims. Check only what you actually ran, and state the result. -->
14
+ - [ ] Tests: `python -m pytest tests/ -q` → N passed
15
+ - [ ] Gates: `<gate command>` → clean
16
+ - [ ] Driven for real: <!-- what you exercised end-to-end, and what you saw -->
17
+
18
+ ## Idea-Id trailer
19
+
20
+ <!-- If a commit here lands an idea recorded in a fleet doc, it carries an
21
+ `Idea-Id: <corpus>-<docslug>-<num>` trailer (add `Idea-Status: partial`
22
+ if it only partly lands it). See CONVENTION.md. Tick when it applies. -->
23
+ - [ ] Not applicable, or the trailer is present on the landing commit(s).
24
+
25
+ ## Out of scope
26
+
27
+ <!-- What this deliberately does not do, and where that work lives. -->
28
+
29
+ ## Next bite
30
+
31
+ <!-- The single next bite this opens, if any. -->
@@ -0,0 +1,64 @@
1
+ name: Release Please
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch: {}
7
+
8
+ permissions:
9
+ contents: write
10
+ pull-requests: write
11
+
12
+ concurrency:
13
+ group: ${{ github.workflow }}-${{ github.ref }}
14
+ cancel-in-progress: false
15
+
16
+ jobs:
17
+ release-please:
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 10
20
+ steps:
21
+ - name: Mint a willow-ci installation token
22
+ id: app-token
23
+ uses: actions/create-github-app-token@v3
24
+ with:
25
+ app-id: ${{ vars.WILLOW_CI_APP_ID }}
26
+ private-key: ${{ secrets.WILLOW_CI_PRIVATE_KEY }}
27
+
28
+ - uses: actions/checkout@v7
29
+ with:
30
+ fetch-depth: 0
31
+ fetch-tags: true
32
+ token: ${{ steps.app-token.outputs.token }}
33
+
34
+ - uses: googleapis/release-please-action@v5
35
+ with:
36
+ token: ${{ steps.app-token.outputs.token }}
37
+ config-file: release-please-config.json
38
+ manifest-file: .release-please-manifest.json
39
+
40
+ - name: Arm auto-merge on the release PR
41
+ env:
42
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
43
+ REPO: ${{ github.repository }}
44
+ run: |
45
+ set -euo pipefail
46
+ pr=$(gh pr list --repo "$REPO" --state open --json number,headRefName \
47
+ --jq '[.[] | select(.headRefName | startswith("release-please--"))]
48
+ | first | .number // empty')
49
+ if [ -z "$pr" ]; then
50
+ echo "No open release PR — nothing releasable since the last tag."
51
+ exit 0
52
+ fi
53
+ # Arming can fail for reasons that are not this job's fault (most
54
+ # commonly: the default branch has no required status checks, so
55
+ # GitHub refuses enablePullRequestAutoMerge). That must not fail the
56
+ # release-please job — but it must not be silent either. A bare
57
+ # `|| true` hid exactly this for six releases, during which every
58
+ # release PR merged immediately and published to PyPI before CI ran.
59
+ if ! out=$(gh pr merge --auto --merge "$pr" --repo "$REPO" 2>&1); then
60
+ echo "::warning::could not arm auto-merge on PR #${pr}: ${out}"
61
+ echo "::warning::the release PR is open and must be merged by hand"
62
+ else
63
+ echo "auto-merge armed on PR #${pr}"
64
+ fi
@@ -0,0 +1,68 @@
1
+ name: Release
2
+
3
+ # Publishes to PyPI when a version tag is pushed (e.g. `git tag v0.2.0 && git push origin v0.2.0`).
4
+ # release-please (release-please.yml) opens the release PR, bumps the version in
5
+ # pyproject.toml (release-type: python) and CHANGELOG.md, and tags on merge.
6
+ #
7
+ # Auth: Trusted Publishing (OIDC). Register on PyPI as:
8
+ # project: willow-reconciler
9
+ # repository: willow-memory/willow-reconciler
10
+ # workflow: release.yml
11
+ # environment: pypi
12
+
13
+ on:
14
+ push:
15
+ tags:
16
+ - "v*"
17
+
18
+ jobs:
19
+ build:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v7
23
+ with:
24
+ fetch-depth: 0
25
+
26
+ - uses: actions/setup-python@v7
27
+ with:
28
+ python-version: "3.11"
29
+
30
+ - name: Build sdist and wheel
31
+ run: |
32
+ python -m pip install --upgrade build
33
+ python -m build
34
+
35
+ - name: Assert the tag matches the built version
36
+ run: |
37
+ tag="${GITHUB_REF_NAME#v}"
38
+ built="$(ls dist/willow_reconciler-*.tar.gz | sed -E 's|.*/willow_reconciler-(.+)\.tar\.gz|\1|')"
39
+ echo "tag=${tag} built=${built}"
40
+ if [ "$tag" != "$built" ]; then
41
+ echo "::error::tag v${tag} builds version ${built} — refusing to publish"
42
+ exit 1
43
+ fi
44
+
45
+ - name: Check metadata
46
+ run: |
47
+ python -m pip install --upgrade twine
48
+ twine check dist/*
49
+
50
+ - uses: actions/upload-artifact@v7
51
+ with:
52
+ name: dist
53
+ path: dist/
54
+
55
+ publish:
56
+ needs: build
57
+ runs-on: ubuntu-latest
58
+ environment: pypi
59
+ permissions:
60
+ id-token: write
61
+ steps:
62
+ - uses: actions/download-artifact@v8
63
+ with:
64
+ name: dist
65
+ path: dist/
66
+
67
+ - name: Publish to PyPI
68
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,39 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test-matrix:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
16
+ steps:
17
+ - uses: actions/checkout@v7
18
+
19
+ - uses: actions/setup-python@v7
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+
23
+ - name: Install
24
+ run: pip install -e ".[test]"
25
+
26
+ - name: Run tests
27
+ run: python -m pytest tests/ -q
28
+
29
+ test:
30
+ needs: [test-matrix]
31
+ if: always()
32
+ runs-on: ubuntu-latest
33
+ steps:
34
+ - name: Assert every matrix leg actually succeeded
35
+ if: ${{ needs.test-matrix.result != 'success' }}
36
+ run: |
37
+ echo "::error::test-matrix did not succeed (result: ${{ needs.test-matrix.result }})"
38
+ exit 1
39
+ - run: echo "all matrix legs green"
@@ -0,0 +1,41 @@
1
+ name: Trailers
2
+
3
+ # Guards the join key the classifier trusts most. `reconciler verify` resolves
4
+ # every Idea-Id trailer in this repo's history against docs/ideas.md and exits
5
+ # non-zero on one that names an item the doc does not contain. A dangling
6
+ # trailer is worse than a missing one: rule 2a asserts LANDED from it ahead of
7
+ # every other inferred signal, and nothing else in the rule stack can tell a
8
+ # real id from a plausible-looking dead one.
9
+ #
10
+ # A history with no trailers passes — the convention is prospective by design.
11
+
12
+ on:
13
+ push:
14
+ branches: [main]
15
+ pull_request:
16
+ branches: [main]
17
+
18
+ jobs:
19
+ verify-trailers:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v7
23
+ with:
24
+ # verify reads the whole history; the default shallow clone would
25
+ # hide every trailer written before the last commit and pass
26
+ # vacuously.
27
+ fetch-depth: 0
28
+
29
+ - uses: actions/setup-python@v7
30
+ with:
31
+ python-version: "3.12"
32
+
33
+ - name: Install
34
+ run: pip install -e .
35
+
36
+ - name: Verify Idea-Id trailers resolve
37
+ # --repo is resolved against the package's own location, not the cwd,
38
+ # so this works from anywhere: on a runner the checkout sits at
39
+ # <work>/willow-reconciler/willow-reconciler, whose parent is the
40
+ # fleet root the sibling lookup expects.
41
+ run: reconciler verify --repo willow-reconciler --doc docs/ideas.md
@@ -0,0 +1,5 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ *.egg-info/
5
+ .venv/
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.2.0"
3
+ }
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Conventional Commits](https://www.conventionalcommits.org/) and this file is
5
+ maintained by [release-please](https://github.com/googleapis/release-please).
6
+
7
+ ## [0.2.0](https://github.com/willow-memory/willow-reconciler/compare/v0.1.0...v0.2.0) (2026-09-11)
8
+
9
+
10
+ ### Added
11
+
12
+ * close the write-side loop for the Idea-Id convention ([717c877](https://github.com/willow-memory/willow-reconciler/commit/717c87786a13df54053ef1c2d13d088517059e74))
13
+ * close the write-side loop for the Idea-Id convention ([d01cdb0](https://github.com/willow-memory/willow-reconciler/commit/d01cdb0af18c1032d83d4e9e0a9a270674bfd140))
14
+
15
+
16
+ ### Fixed
17
+
18
+ * anchor legend-tag regexes so a mid-prose marker is not a tag ([9c8be17](https://github.com/willow-memory/willow-reconciler/commit/9c8be17416898ebc6c7ff80811e9a127ccd47736))
19
+ * close two false-LANDED paths found by adversarial audit ([46d18bd](https://github.com/willow-memory/willow-reconciler/commit/46d18bd5bbc9b4065ddf11a005cddb8df59f66b5))
20
+
21
+ ## 0.1.0 (2026-09-11)
22
+
23
+
24
+ ### Added
25
+
26
+ * Initial Slice 0 idea-to-landing reconciler: reads a fleet doc's numbered
27
+ items and the target repo's own git history, then reports — deterministically
28
+ and stdlib-only, with no model call — how many proposed ideas have landed,
29
+ partially landed, or show no evidence of having started.
30
+ * An honest, non-circular hold-out acceptance test (`reconciler validate`
31
+ wiring in `validate.py`): the legend tag is stripped from each hand-tagged
32
+ item before reclassification, so the score measures what the tool can
33
+ actually recover rather than echoing a tag it just read.
34
+ * The `Idea-Id` commit-trailer convention (see `CONVENTION.md`) that gives the
35
+ reconciler a durable join key going forward.
@@ -0,0 +1,39 @@
1
+ # Code of Conduct
2
+
3
+ This organization adopts the [Contributor Covenant, version 2.1][covenant], in full and without
4
+ modification, as the code of conduct for every repository here, including
5
+ willow-memory/willow-reconciler.
6
+
7
+ The canonical text is at
8
+ <https://www.contributor-covenant.org/version/2/1/code_of_conduct/>.
9
+
10
+ In short: participate in a way that would not embarrass you if quoted. Assume good faith,
11
+ disagree about the work rather than the person, and accept that maintainers may decline a
12
+ contribution without that being an insult.
13
+
14
+ ## Scope
15
+
16
+ This applies in every space the organization controls — issues, pull requests, discussions,
17
+ commit messages, and code review across all repositories — and whenever someone is representing
18
+ the project in public.
19
+
20
+ ## Reporting
21
+
22
+ Report unacceptable behavior to **[@rudi193-cmd](https://github.com/rudi193-cmd)** by opening a
23
+ private report through GitHub's **"Report a vulnerability"** form on this organization's
24
+ [`.github` Security tab][report]. Say plainly that it is a conduct report; it will not be
25
+ treated as a security matter.
26
+
27
+ [report]: https://github.com/willow-memory/.github/security/advisories/new
28
+
29
+ If the report concerns that maintainer, contact any other organization owner directly.
30
+
31
+ ## Enforcement
32
+
33
+ Maintainers may take any action they judge proportionate, including editing or removing
34
+ contributions, and temporary or permanent bans from the organization. The Contributor Covenant's
35
+ enforcement guidelines describe the ladder that is normally followed.
36
+
37
+ ---
38
+
39
+ [covenant]: https://www.contributor-covenant.org/version/2/1/code_of_conduct/
@@ -0,0 +1,44 @@
1
+ # Contributing
2
+
3
+ This repo (willow-memory/willow-reconciler) shares one working method with the
4
+ rest of the fleet, whether the contributor is a person or an agent.
5
+
6
+ ## The method
7
+
8
+ - **One bite at a time.** A PR delivers one outcome.
9
+ - **Receipts, not claims.** The PR template asks for Evidence — check
10
+ only what you actually ran, and state the result.
11
+ - **Verify in a clean environment.** If you touched dependencies, prove
12
+ the suite in a clean environment before pushing.
13
+ - **Match the house style.** Read the surrounding code first — this is a
14
+ stdlib-only, deterministic, read-only tool; keep it that way (no runtime
15
+ dependencies, no model calls).
16
+
17
+ ## Build and test
18
+
19
+ This package is stdlib-only; the only extra is `test` (pytest):
20
+
21
+ ```sh
22
+ pip install -e ".[test]"
23
+ python -m pytest tests/ -q
24
+ ```
25
+
26
+ Requires Python 3.10+. CI runs the suite across 3.10–3.14 and must be green
27
+ before merge.
28
+
29
+ ## The Idea-Id commit-trailer convention
30
+
31
+ This repo's own convention: a commit that lands an idea recorded in a fleet doc
32
+ carries an `Idea-Id: <corpus>-<docslug>-<num>` git trailer (add
33
+ `Idea-Status: partial` when a commit only partly lands it). It is the durable
34
+ join key the reconciler reads. Emit it when a commit lands an idea. See
35
+ [CONVENTION.md](CONVENTION.md) for the full rule and what counts as landing
36
+ evidence.
37
+
38
+ ## Practical bits
39
+
40
+ - PRs use the closeout template (Bite / What was done / Evidence /
41
+ Out of scope / Next bite).
42
+ - Security findings go through [SECURITY.md](SECURITY.md), not issues.
43
+
44
+ See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) and [SUPPORT.md](SUPPORT.md).
@@ -0,0 +1,85 @@
1
+ # The Idea-Id trailer convention
2
+
3
+ Adopted 2026-09-11 (operator ruling: "adopt the trailers going forward").
4
+ Recorded as governance decision `decision-2026-09-11-adopt-idea-id-trailers`;
5
+ proposed to Nestor as draft `11a1744c-6b83-48f5-9fd8-d7a929b5eabf` (seal pending).
6
+
7
+ ## What
8
+
9
+ A commit that lands an idea recorded in a fleet doc carries a git trailer:
10
+
11
+ Idea-Id: <corpus>-<docslug>-<num>
12
+
13
+ matching the id this reconciler derives for that doc entry
14
+ (e.g. `willow-ideas-042` for the 42nd entry of willow-mcp/docs/ideas.md).
15
+ When a commit only partly lands an idea, add:
16
+
17
+ Idea-Status: partial
18
+
19
+ The default (trailer present, no status) means landed.
20
+
21
+ ## Why it is prospective, not retroactive
22
+
23
+ willow-reconciler Slice 0 measured, on a held-out split of the *current*
24
+ corpus with tags stripped, a landing-recovery of **0.0**: no durable join
25
+ key was ever written, so deterministic inference recovers nothing, and a
26
+ model-based guess would violate the cheapest-capable rule. The ID's absence
27
+ is itself why "format predicts landing." The fix cannot look backward — it
28
+ starts writing the key now, and this reconciler grows useful as trailers
29
+ accumulate.
30
+
31
+ ## Tooling (you should not be typing these by hand)
32
+
33
+ reconciler id --repo <repo> --doc <doc> --num 42 # print the trailer line
34
+ reconciler id --repo <repo> --doc <doc> --grep "text" # ... or find it by text
35
+ reconciler install-hook --repo <repo> # write it automatically
36
+ reconciler verify --repo <repo> --doc <doc> # check they all resolve
37
+
38
+ `install-hook` adds a `prepare-commit-msg` hook that derives the trailer from
39
+ a branch name that names an idea number (`idea-42`, `ideas-042`,
40
+ `feature/idea_42-thing`), and a `commit-msg` hook that rejects a malformed
41
+ `Idea-Id` or `Idea-Status` line. Neither hook can invent a link: the first
42
+ fires only when the branch already carries the number, and it never overwrites
43
+ a trailer written by hand.
44
+
45
+ A commit may carry several `Idea-Id` trailers when it lands several ideas.
46
+ `Idea-Status` is commit-level, so a commit saying `partial` says it about
47
+ every id it names.
48
+
49
+ ## A wrong id is worse than no id
50
+
51
+ An `Idea-Id` that resolves to nothing is not a harmless typo. Rule 2a asserts
52
+ LANDED from a trailer ahead of every other inferred signal, and nothing else in
53
+ the rule stack can distinguish a real join key from a plausible-looking dead
54
+ one — so a dangling trailer is a silent, permanent, confident wrong answer.
55
+ That is why the id shape is fixed (`willow-ideas-NNN`, three digits), why the
56
+ commit-msg hook rejects anything else, and why `reconciler verify` runs in CI.
57
+
58
+ ## Nothing to backfill on the doc side
59
+
60
+ The doc-side id is *derived* by `reconciler/ids.py` from the entry's position
61
+ in the doc — it is not stored in the doc. So no edit to ideas.md is required.
62
+ The only new discipline is on the commit side: emit the trailer when a commit
63
+ lands an idea. This sits alongside the `Co-Authored-By` / `Claude-Session`
64
+ trailers the harness already appends, so it is one more line.
65
+
66
+ ## What counts as landing evidence
67
+
68
+ - The `Idea-Id` trailer on a commit **reachable from the checkout being
69
+ reconciled** (primary key). Evidence is scoped to `git log HEAD`, not
70
+ `git log --all`: a trailer on an abandoned or rejected branch is not a
71
+ landing, and reading it as one was a false LANDED sourced from the ref scope.
72
+ - A commit body that says "PR #N" *and* git shows PR #N merged **as a real
73
+ merge commit** (two or more parents, with GitHub's own merge subject). A bare
74
+ `#N`, or an idea-number cross-reference like "former #103", is NOT a PR ref —
75
+ the reconciler rejects it (see tests/).
76
+
77
+ Deliberately NOT evidence: a subject ending `(#N)`. That is GitHub's
78
+ squash-merge title shape, but it is also the ordinary conventional-commit habit
79
+ of naming an issue, and the two are indistinguishable from git alone — it
80
+ asserted LANDED for unrelated commits. Repos that squash-merge should rely on
81
+ the trailer, which is the durable key regardless of merge strategy.
82
+
83
+ A legend tag in the doc is read as a tag only when it LEADS the item text or
84
+ when its dash-introduced clause NAMES its legend keyword (`shipped` / `partial`).
85
+ An ordinary sentence containing an em-dash and a marker is prose, not a tag.