scar-cli 0.4.0__tar.gz → 0.5.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 (77) hide show
  1. {scar_cli-0.4.0 → scar_cli-0.5.0}/.scars/0001-git-grep-ere-pitfalls.landmine.md +3 -5
  2. {scar_cli-0.4.0 → scar_cli-0.5.0}/.scars/0002-agent-direct-hook-install.deadend.md +4 -4
  3. {scar_cli-0.4.0 → scar_cli-0.5.0}/.scars/0005-history-rewrite-orphans-commit-evidence.landmine.md +13 -8
  4. scar_cli-0.5.0/.scars/0006-yaml-pattern-anchor-over-escaping.landmine.md +41 -0
  5. scar_cli-0.5.0/.scars/candidates/fp-log.txt +3 -0
  6. {scar_cli-0.4.0 → scar_cli-0.5.0}/CHANGELOG.md +18 -0
  7. {scar_cli-0.4.0 → scar_cli-0.5.0}/PKG-INFO +1 -1
  8. {scar_cli-0.4.0 → scar_cli-0.5.0}/ROADMAP.md +4 -4
  9. scar_cli-0.5.0/experiments/harvest/PROTOCOL.md +76 -0
  10. {scar_cli-0.4.0 → scar_cli-0.5.0}/pyproject.toml +1 -1
  11. scar_cli-0.5.0/src/scar/cli.py +602 -0
  12. scar_cli-0.5.0/src/scar/evidence.py +86 -0
  13. scar_cli-0.5.0/src/scar/harvest.py +351 -0
  14. {scar_cli-0.4.0 → scar_cli-0.5.0}/src/scar/match.py +20 -7
  15. scar_cli-0.5.0/src/scar/orphan.py +233 -0
  16. scar_cli-0.5.0/tests/test_cli.py +651 -0
  17. scar_cli-0.5.0/tests/test_evidence.py +134 -0
  18. scar_cli-0.5.0/tests/test_harvest.py +298 -0
  19. scar_cli-0.5.0/tests/test_orphan.py +398 -0
  20. {scar_cli-0.4.0 → scar_cli-0.5.0}/uv.lock +1 -1
  21. scar_cli-0.4.0/.scars/candidates/fp-log.txt +0 -1
  22. scar_cli-0.4.0/src/scar/cli.py +0 -288
  23. scar_cli-0.4.0/src/scar/harvest.py +0 -111
  24. scar_cli-0.4.0/tests/test_cli.py +0 -151
  25. scar_cli-0.4.0/tests/test_harvest.py +0 -63
  26. {scar_cli-0.4.0 → scar_cli-0.5.0}/.github/workflows/ci.yml +0 -0
  27. {scar_cli-0.4.0 → scar_cli-0.5.0}/.github/workflows/pr-validation.yml +0 -0
  28. {scar_cli-0.4.0 → scar_cli-0.5.0}/.github/workflows/release.yml +0 -0
  29. {scar_cli-0.4.0 → scar_cli-0.5.0}/.gitignore +0 -0
  30. {scar_cli-0.4.0 → scar_cli-0.5.0}/.scars/0003-installer-binds-to-active-venv-scar.landmine.md +0 -0
  31. {scar_cli-0.4.0 → scar_cli-0.5.0}/.scars/0004-promote-roundtrip-drops-expires-evidence.landmine.md +0 -0
  32. {scar_cli-0.4.0 → scar_cli-0.5.0}/.scars/README.md +0 -0
  33. {scar_cli-0.4.0 → scar_cli-0.5.0}/.scars/template.md +0 -0
  34. {scar_cli-0.4.0 → scar_cli-0.5.0}/AGENTS.md +0 -0
  35. {scar_cli-0.4.0 → scar_cli-0.5.0}/CONTRIBUTING.md +0 -0
  36. {scar_cli-0.4.0 → scar_cli-0.5.0}/IDEA.md +0 -0
  37. {scar_cli-0.4.0 → scar_cli-0.5.0}/LICENSE +0 -0
  38. {scar_cli-0.4.0 → scar_cli-0.5.0}/README.md +0 -0
  39. {scar_cli-0.4.0 → scar_cli-0.5.0}/SCAR-FORMAT.md +0 -0
  40. {scar_cli-0.4.0 → scar_cli-0.5.0}/SPEC.md +0 -0
  41. {scar_cli-0.4.0 → scar_cli-0.5.0}/STRESS-TEST.md +0 -0
  42. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/anchor-survival/PROTOCOL.md +0 -0
  43. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/anchor-survival/RESULTS.md +0 -0
  44. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/anchor-survival/long_replay.py +0 -0
  45. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/anchor-survival/replay.py +0 -0
  46. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/auto-authorship/FINDINGS.md +0 -0
  47. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/auto-authorship/PROTOCOL.md +0 -0
  48. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/fence-honor/.gitignore +0 -0
  49. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/fence-honor/PROTOCOL.md +0 -0
  50. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/fence-honor/RESULTS.md +0 -0
  51. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/fence-honor/fixture/.scars/0001-vendor-retry-window.fence.md +0 -0
  52. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/fence-honor/fixture/.scars/0002-evicting-session-store.deadend.md +0 -0
  53. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/fence-honor/fixture/.scars/0003-export-column-order.landmine.md +0 -0
  54. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/fence-honor/fixture/README.md +0 -0
  55. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/fence-honor/fixture/payments/retry.py +0 -0
  56. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/fence-honor/fixture/reports/export.py +0 -0
  57. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/fence-honor/fixture/services/sessions.py +0 -0
  58. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/fence-honor/grade.py +0 -0
  59. {scar_cli-0.4.0 → scar_cli-0.5.0}/experiments/harvest/harvest.py +0 -0
  60. {scar_cli-0.4.0 → scar_cli-0.5.0}/hook/scar-hooks.py +0 -0
  61. {scar_cli-0.4.0 → scar_cli-0.5.0}/src/scar/__init__.py +0 -0
  62. {scar_cli-0.4.0 → scar_cli-0.5.0}/src/scar/agent.py +0 -0
  63. {scar_cli-0.4.0 → scar_cli-0.5.0}/src/scar/hooks.py +0 -0
  64. {scar_cli-0.4.0 → scar_cli-0.5.0}/src/scar/installer.py +0 -0
  65. {scar_cli-0.4.0 → scar_cli-0.5.0}/src/scar/lint.py +0 -0
  66. {scar_cli-0.4.0 → scar_cli-0.5.0}/src/scar/mcp.py +0 -0
  67. {scar_cli-0.4.0 → scar_cli-0.5.0}/src/scar/model.py +0 -0
  68. {scar_cli-0.4.0 → scar_cli-0.5.0}/src/scar/render.py +0 -0
  69. {scar_cli-0.4.0 → scar_cli-0.5.0}/src/scar/store.py +0 -0
  70. {scar_cli-0.4.0 → scar_cli-0.5.0}/tests/test_hooks.py +0 -0
  71. {scar_cli-0.4.0 → scar_cli-0.5.0}/tests/test_installer.py +0 -0
  72. {scar_cli-0.4.0 → scar_cli-0.5.0}/tests/test_lifecycle.py +0 -0
  73. {scar_cli-0.4.0 → scar_cli-0.5.0}/tests/test_lint.py +0 -0
  74. {scar_cli-0.4.0 → scar_cli-0.5.0}/tests/test_match.py +0 -0
  75. {scar_cli-0.4.0 → scar_cli-0.5.0}/tests/test_mcp.py +0 -0
  76. {scar_cli-0.4.0 → scar_cli-0.5.0}/tests/test_model.py +0 -0
  77. {scar_cli-0.4.0 → scar_cli-0.5.0}/tests/test_store.py +0 -0
@@ -8,12 +8,10 @@ created: 2026-06-09
8
8
  authors: ["claude-code", kibukx]
9
9
  anchors:
10
10
  - path: experiments/anchor-survival/
11
- - path: hook/scar-precheck.py
12
- - pattern: "git.{0,20}grep.{0,40}\\\\b"
11
+ - path: src/scar/harvest.py
12
+ - pattern: "git.{0,20}grep"
13
13
  evidence:
14
- - commit: 5c63b14
15
- - note: "produced a fake 0% anchor-survival run before diagnosis (gate 0.2)"
16
- - note: "history rewritten at v0.1.0 public release; pre-release SHAs resolve on GitHub by URL but not in fresh clones"
14
+ - note: "orphaned receipt — pre-v0.1.0 commit 5c63b14 produced a fake 0% anchor-survival run before diagnosis (gate 0.2); resolves at github.com/Daily-Nerd/Scar/commit/5c63b14 until GC, not in fresh clones (rewritten at the v0.1.0 release)"
17
15
  expires:
18
16
  condition: "resolver layer gains integration tests over its git invocations"
19
17
  review_after: 2027-06-09
@@ -8,11 +8,11 @@ created: 2026-06-09
8
8
  authors: ["claude-code", kibukx]
9
9
  anchors:
10
10
  - path: hook/
11
- - pattern: "settings\\.json.{0,80}hooks|hooks.{0,80}settings\\.json"
11
+ - path: src/scar/installer.py
12
12
  evidence:
13
- - commit: faad8f6
14
- - commit: bcd3864
15
- - note: "history rewritten at v0.1.0 public release; pre-release SHAs resolve on GitHub by URL but not in fresh clones"
13
+ - note: "orphaned receipt — pre-v0.1.0 installer commit faad8f6 at github.com/Daily-Nerd/Scar/commit/faad8f6 (unreachable in fresh clones)"
14
+ - note: "orphaned receipt — pre-v0.1.0 hooks commit bcd3864 at github.com/Daily-Nerd/Scar/commit/bcd3864 (unreachable in fresh clones)"
15
+ - note: "both SHAs orphaned by the fresh-start force-push at the v0.1.0 public release; resolve on GitHub by URL until GC, not in fresh clones"
16
16
  status: active
17
17
  ---
18
18
 
@@ -8,7 +8,6 @@ created: 2026-06-11
8
8
  authors: ["claude-code", "Kibukx"]
9
9
  anchors:
10
10
  - path: .scars/
11
- - pattern: "push.{0,30}(--force|\\+[a-zA-Z]).{0,40}main|filter-repo|checkout --orphan"
12
11
  evidence:
13
12
  - note: v0.1.0 public release (2026-06-11): fresh-start force-push orphaned 3 commit SHAs cited by scars 0001 and 0002; SHAs still resolve on GitHub by URL but fail in any fresh clone, and GitHub may GC them eventually
14
13
  expires:
@@ -19,7 +18,7 @@ status: active
19
18
 
20
19
  Scars cite commit SHAs as evidence receipts. Those receipts implicitly assume
21
20
  the SHA stays reachable in the repo's history forever. Any history rewrite —
22
- fresh-start orphan branch, force-push, filter-repo scrub — silently breaks
21
+ fresh-start orphan branch, force-push, filter-repo scrub, or a routine squash-/rebase-merge — silently breaks
23
22
  that assumption: the scar still lints clean and still fires (anchors are
24
23
  paths/patterns, not commits), but `git show <sha>` fails in every fresh clone,
25
24
  so the receipt is unverifiable exactly where strangers would check it.
@@ -29,9 +28,15 @@ Observed at the v0.1.0 public release: the fresh-start force-push orphaned
29
28
  until after the push because nothing in the toolchain connects "history
30
29
  operation" to "evidence integrity."
31
30
 
32
- Before any history rewrite in a repo with scars: grep `.scars/` for
33
- `commit:` evidence and either (a) amend those scars with a note explaining the
34
- rewrite, (b) replace bare SHAs with full GitHub commit URLs (survive as
35
- unreachable objects, at GC's mercy), or (c) inline the relevant diff/fact into
36
- a note so the scar is self-contained. Longer term: `scar lint` could warn
37
- when a cited SHA is unreachable from HEAD.
31
+ The everyday trigger is the merge strategy itself: this repo squash-merges, so a
32
+ feature-branch commit exactly the SHA you cite while drafting a scar mid-PR
33
+ is orphaned the moment that PR lands. Rebase-merge does the same; only a true
34
+ merge-commit preserves branch SHAs. So PREFER `pr:`/`issue:` evidence (it
35
+ resolves on GitHub regardless of merge strategy) or a SHA already on the default
36
+ branch, and avoid citing transient feature-branch SHAs at all.
37
+
38
+ Before any deliberate history rewrite: grep `.scars/` for `commit:` evidence and
39
+ either (a) amend with a note explaining the rewrite, (b) replace bare SHAs with
40
+ full GitHub commit URLs (at GC's mercy), or (c) inline the fact so the scar is
41
+ self-contained. `scar lint` now warns when a cited SHA is unreachable from HEAD
42
+ (#43) — but it fires after the fact; the durable fix is not citing branch SHAs.
@@ -0,0 +1,41 @@
1
+ ---
2
+ id: 6
3
+ type: landmine
4
+ title: Pattern anchors over-escape through YAML double-quotes and silently only self-match
5
+ severity: medium
6
+ confidence: 0.9
7
+ created: 2026-06-13
8
+ authors: ["claude-code", "kibukx"]
9
+ anchors:
10
+ - path: src/scar/orphan.py
11
+ - path: src/scar/match.py
12
+ - path: .scars/
13
+ evidence:
14
+ - pr: 40
15
+ - note: scar 1 grep pattern matched only its own body, never experiments/anchor-survival/RESULTS.md
16
+ expires:
17
+ condition: "pattern anchors are authored through a validated path (e.g. scar draft) that escapes regex correctly, OR lint rejects a pattern whose only pre-exclusion match is the scar's own file"
18
+ review_after: 2027-06-13
19
+ status: active
20
+ ---
21
+
22
+ A regex written in a scar's `pattern:` field passes through YAML double-quoted
23
+ string parsing before it ever reaches the matcher. Backslashes collapse: what
24
+ you type as a four-backslash word boundary in the file becomes a regex needing
25
+ *literal* backslashes, not a word boundary. The intended code almost never
26
+ contains literal backslashes, so the pattern matches nothing real.
27
+
28
+ The trap is that it still reads as LIVE. Pattern anchors are matched against ALL
29
+ tracked content, including the scar's own `.scars/` body, and the body quotes
30
+ the pattern verbatim, so the scar keeps itself alive by self-reference. Orphan
31
+ detection sees a live anchor and stays quiet. The protection is dead; the gauge
32
+ says green. On this repo, scars 1 and 5 were pure ghosts (own-body only) and
33
+ scar 2 matched zero files at all, none visible until self-referential exclusion
34
+ was added in PR #40 (`_pattern_anchor_live(..., exclude_path=self_path)`).
35
+
36
+ What a future editor must do: when adding a `pattern:` anchor, verify it matches
37
+ the REAL code with `scar lint` (it must NOT appear under partial-rot), not just
38
+ that the scar parses. Prefer a `path:` anchor when the target is a file or dir;
39
+ path anchors do not go through regex escaping and cannot self-match. If you must
40
+ use a regex with escapes, test it against tracked content excluding the scar's
41
+ own file before trusting it.
@@ -0,0 +1,3 @@
1
+ 2026-06-12 false trigger: meta-session — we tuned the revert-language detector itself, so assistant prose ('revert language', 'reverting' in test fixtures/PR text) matched REVERT_RE; nothing abandoned (tool_errors were expected CLI probes/rejections). First post-tune FP pattern: self-referential sessions about the drafter trip the drafter.
2
+ 2026-06-13 false trigger: tool_errors were external API hiccups (pypistats rate-limit/404, bq schema field); no code approach tried-and-abandoned this session (design-only work)
3
+ 2026-06-12 false trigger: orphan-detection impl — 'revert' is feature-domain ('revert case' reverse hint = anchors-live-again) + a planned AC#1 refactor swapping batch-1 copied anchor logic for a shared match.py primitive; replacement was design-mandated, not a deadend discovered by failure
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0](https://github.com/Daily-Nerd/Scar/compare/v0.4.0...v0.5.0) (2026-06-13)
4
+
5
+
6
+ ### Features
7
+
8
+ * **harvest:** precision@N reporting CLI — close the measurement loop ([#53](https://github.com/Daily-Nerd/Scar/issues/53)) ([100bd1d](https://github.com/Daily-Nerd/Scar/commit/100bd1d46bbac981a3629b74c237fc0584f5ce05))
9
+ * **harvest:** ranking layer — heuristic scorer + label-capture instrument ([#39](https://github.com/Daily-Nerd/Scar/issues/39)) ([7369f73](https://github.com/Daily-Nerd/Scar/commit/7369f738d3fe356a0290cbf05f0654a48587ee9f))
10
+ * **lifecycle:** lint warns on evidence commit SHAs unreachable from HEAD ([#44](https://github.com/Daily-Nerd/Scar/issues/44)) ([714357e](https://github.com/Daily-Nerd/Scar/commit/714357e9b6366ec67d71d086cf62d8dafbcae976))
11
+ * **lifecycle:** orphan detection — resolution failure, loud in CI ([#34](https://github.com/Daily-Nerd/Scar/issues/34)) ([421a12a](https://github.com/Daily-Nerd/Scar/commit/421a12aae25cc46f6aa40593a6274bb755d4b81b))
12
+ * **lifecycle:** partial-anchor rot — surface dead anchors on firing scars ([#40](https://github.com/Daily-Nerd/Scar/issues/40)) ([85fd57e](https://github.com/Daily-Nerd/Scar/commit/85fd57e397055576bd754c3d606417274d6a9d5c))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **scars:** drop [#6](https://github.com/Daily-Nerd/Scar/issues/6) orphaned receipt, broaden scar [#5](https://github.com/Daily-Nerd/Scar/issues/5) for squash-merge ([#51](https://github.com/Daily-Nerd/Scar/issues/51)) ([4c63ac5](https://github.com/Daily-Nerd/Scar/commit/4c63ac50c648d8ec47190c6045987a276c9fb9bf))
18
+ * **scars:** re-anchor 3 ghost pattern anchors to real code ([#42](https://github.com/Daily-Nerd/Scar/issues/42)) ([00a2fcb](https://github.com/Daily-Nerd/Scar/commit/00a2fcb5c41c165f260019ec95bc636b18d17491))
19
+ * **scars:** replace 3 orphaned bare commit-SHA receipts with self-contained notes ([#46](https://github.com/Daily-Nerd/Scar/issues/46)) ([a224619](https://github.com/Daily-Nerd/Scar/commit/a224619f47387cce401039bf9ddbb93cb3841641))
20
+
3
21
  ## [0.4.0](https://github.com/Daily-Nerd/Scar/compare/v0.3.0...v0.4.0) (2026-06-12)
4
22
 
5
23
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scar-cli
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: SCAR — version control for negative knowledge (deadends, fences, landmines)
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -29,11 +29,11 @@ Public at [github.com/Daily-Nerd/Scar](https://github.com/Daily-Nerd/Scar), `sca
29
29
 
30
30
  - ✅ **MCP server** (`scar_query`, `scar_why`, `scar_draft`) — shipped v0.3.0, dependency-free stdio, drafts gated to candidates/. First non-Claude agent (Codex) arrived and contributed the implementation — the deferral condition resolving itself.
31
31
  - ✅ Multi-agent surface: committed `AGENTS.md`, `scar inject --diff`, `scar agent doctor/config` for Codex, Cursor, Windsurf, opencode (v0.3.0)
32
- - 🔶 CI surface: expiry warnings shipped (`lint`/`status`, v0.2.0); **orphan detection is the next milestone** content-fingerprint drift → `orphaned` status, loud in CI (principle 3 is not yet enforced by code)
33
- - Harvest ranking layer (gate 0.1 verdict: required raw precision 13% without it)
34
- - ⬜ Re-anchoring agent workflow: orphaned scar + orphaning diff → proposed new anchors as a PR
32
+ - CI surface: expiry warnings (`lint`/`status`, v0.2.0); orphan detection — all-anchors-dead firing scars → `orphaned`, loud in CI (#34); partial-rot advisory firing scars with ≥1 dead anchor among live ones, named in `lint`/`status`/`orphan` (#35). Principle 3 now enforced by code for both total and partial rot.
33
+ - Harvest ranking layer — heuristic weighted scorer + label-capture instrument, zero-dep, deterministic (#39). Weights remain intuition until real-repo labels calibrate precision.
34
+ - ⬜ Re-anchoring agent workflow: orphaned/partially-rotted scar + orphaning diff → proposed new anchors as a PR
35
35
  - ⬜ Editor surfaces (VS Code gutter marks, LSP code lens) — fences visible to humans, not only agents
36
- - Lint warning on evidence commit SHAs unreachable from HEAD (scar #5's expiry condition)
36
+ - Lint warning on evidence commit SHAs unreachable from HEAD (#43) — scar #5's expiry condition, now enforced; advisory in `lint`, skipped on shallow clones
37
37
 
38
38
  ## Phase 3 — The org graph ⏸ parked by design
39
39
 
@@ -0,0 +1,76 @@
1
+ # Experiment: Harvest Ranking + Label Instrument (Issue #38)
2
+
3
+ **Question.** Can a cheap, explainable heuristic score RANK harvested candidates so the human curator reads the real scars first — without normalizing away the precision signal carried by candidate type?
4
+
5
+ **Why it matters.** Raw harvest precision sits at ~13% on real history. If a curator must read every candidate in arbitrary order, the tool costs more attention than it saves. Ranking earns its place only if the top-N is denser in real scars than the tail. This experiment builds the *instrument* (labels + precision@N) so that claim becomes measurable instead of asserted.
6
+
7
+ ## What the ranker does
8
+
9
+ Each candidate gets a deterministic `score` (see `src/scar/harvest.py`). The score is a sum of calibration priors — base weight per signal type plus small bonuses (PR/issue ref on reverts, files-deleted threshold, oscillation count, comment specificity, recency). All weights are **priors, unvalidated until labels exist**; this experiment is how they get validated.
10
+
11
+ **Cross-section ranking uses RAW score, no normalization** (`scar harvest --top-k N`). The per-type base constants order `comment < flapping < deleted_component < revert`. That ordering is an intentional precision prior: signal *type* predicts precision, so a revert outranks a grep hit by design. Normalizing scores across types would erase exactly the signal we want to exploit. If the labels later show the ordering is wrong, fix the base constants — do not add normalization.
12
+
13
+ ## Label JSONL format
14
+
15
+ Path: `experiments/harvest/labels.jsonl` (committed — instrument/data, like the anchor-survival replay). Written one line at a time by:
16
+
17
+ ```
18
+ scar harvest <repo> --label <id> keep|discard [--note "..."]
19
+ ```
20
+
21
+ Each line is one JSON object:
22
+
23
+ | Field | Type | Meaning |
24
+ |---------|--------|---------|
25
+ | `id` | string | the candidate's stable id (see below) |
26
+ | `label` | string | **exactly** `"keep"` or `"discard"` — nothing else is accepted |
27
+ | `note` | string | free-text rationale (may be empty) |
28
+ | `date` | string | `YYYY-MM-DD`, from `time.strftime` (monkeypatchable in tests) |
29
+ | `repo` | string | the harvested repo's name (provenance) |
30
+
31
+ **Only `keep`/`discard` are valid.** The CLI rejects any other label value with a non-zero exit and writes nothing. This is load-bearing: `precision_at_n` reads `label == "keep"` and counts everything labeled as the denominator — a third value (`"maybe"`, `"skip"`) would silently corrupt precision by inflating the denominator without ever counting toward the numerator.
32
+
33
+ **Id validation.** `--label` runs `harvest(repo)` for the target repo, collects every candidate id, and **rejects an id not in that set** (mirrors `scar orphan --apply` rejecting an unknown `--id`). You cannot label a candidate that the current harvest does not produce.
34
+
35
+ ## Candidate-id stability rule
36
+
37
+ `harvest.candidate_id(signal_type, candidate)` = first 10 hex of `sha1(signal_type + identifying-fields)`. The id is a hash of the **identifying fields only — NOT the score, NOT the id itself**, so the same candidate gets the same id across runs and a re-scored candidate keeps its label.
38
+
39
+ Identifying fields per type:
40
+
41
+ | Type | Hashed fields |
42
+ |---------------------|---------------|
43
+ | `revert` | `commit` |
44
+ | `deleted_component` | `component` |
45
+ | `flapping` | `file` + `key` |
46
+ | `comment` | `location` + `text[:40]` |
47
+
48
+ **Comment ids use `text[:40]`** — the first 40 characters of the comment text. Keep those 40 chars stable: editing the tail of a long comment preserves the id; editing the start changes it (and orphans any prior label). This deliberately tolerates the 120-char display truncation in `_comment_archaeology` without making the id depend on it.
49
+
50
+ ## Precision@N
51
+
52
+ `harvest.precision_at_n(ranked, labels, n)`:
53
+ - `ranked` — candidates pre-sorted by score descending (caller's responsibility; `scar harvest --top-k` produces this order).
54
+ - `labels` — a `{id: "keep"|"discard"}` dict built from the JSONL (group by id; last write wins if a candidate was labeled twice).
55
+ - Take the first `n`. Among them, consider **only** candidates whose id is in `labels`. Return the fraction of that labeled subset where `label == "keep"`.
56
+
57
+ **Contract: unlabeled candidates in the top-N are excluded from BOTH numerator and denominator.** They neither help nor hurt the score — precision@N measures "of the ones we judged in the top-N, how many were real". If no candidate in the top-N is labeled, the result is `0.0` (not NaN, not an error).
58
+
59
+ ## Method (to run once labels accrue)
60
+
61
+ 1. Harvest a real repo; curate the top-N by hand, recording `keep`/`discard` via `--label`.
62
+ 2. Build `{id: label}` from `labels.jsonl`.
63
+ 3. Compute `precision_at_n` at several N (e.g. 5, 10, 20) and compare against the ~13% raw base rate.
64
+ 4. Compare per-type precision to validate (or refute) the base-constant ordering.
65
+
66
+ ## Pre-registered claim
67
+
68
+ - **Ranking earns its place** if precision@N for small N is materially above the ~13% raw base rate — i.e. the top of the ranked list is denser in real scars than the unranked pool.
69
+ - If precision@N ≈ base rate at every N, the score adds no signal and the constants need rework (or the heuristic is the wrong instrument).
70
+
71
+ ## Limitations (declared)
72
+
73
+ 1. Weights are hand-set priors, not fit to data — this instrument exists to replace the guess with a measurement, but until ~50 labels accrue the ranking is an assertion.
74
+ 2. Single-curator labels carry that curator's bias; `keep`/`discard` is a coarse binary over what is really a confidence gradient.
75
+ 3. `precision_at_n` ignores recall — a candidate the harvester never surfaced cannot be labeled, so a missed real scar is invisible here.
76
+ 4. Recency scoring reads the wall clock at harvest time; the same candidate scored months apart can shift rank (id stays stable, so labels still attach correctly).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scar-cli"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "SCAR — version control for negative knowledge (deadends, fences, landmines)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"