scar-cli 0.6.1__tar.gz → 0.7.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.
- scar_cli-0.7.0/.release-please-manifest.json +3 -0
- scar_cli-0.7.0/.scars/0007-release-please-config-change-skips-open-pr.landmine.md +38 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/.scars/candidates/fp-log.txt +1 -0
- {scar_cli-0.6.1/plugin/skills/scar-authoring/assets → scar_cli-0.7.0/.scars}/template.md +4 -1
- {scar_cli-0.6.1 → scar_cli-0.7.0}/CHANGELOG.md +13 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/PKG-INFO +1 -1
- {scar_cli-0.6.1 → scar_cli-0.7.0}/plugin/plugin.json +1 -1
- {scar_cli-0.6.1/src/scar → scar_cli-0.7.0/plugin}/skills/scar-authoring/assets/template.md +4 -1
- {scar_cli-0.6.1 → scar_cli-0.7.0}/pyproject.toml +1 -1
- {scar_cli-0.6.1 → scar_cli-0.7.0}/src/scar/harvest.py +21 -5
- {scar_cli-0.6.1 → scar_cli-0.7.0}/src/scar/lint.py +6 -1
- {scar_cli-0.6.1 → scar_cli-0.7.0}/src/scar/model.py +19 -2
- {scar_cli-0.6.1/.scars → scar_cli-0.7.0/src/scar/skills/scar-authoring/assets}/template.md +4 -1
- {scar_cli-0.6.1 → scar_cli-0.7.0}/src/scar/store.py +7 -3
- {scar_cli-0.6.1 → scar_cli-0.7.0}/tests/test_harvest.py +43 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/tests/test_lint.py +29 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/tests/test_model.py +79 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/tests/test_store.py +15 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/uv.lock +1 -1
- scar_cli-0.6.1/.release-please-manifest.json +0 -3
- {scar_cli-0.6.1 → scar_cli-0.7.0}/.claude-plugin/marketplace.json +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/.github/workflows/ci.yml +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/.github/workflows/pr-validation.yml +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/.github/workflows/release.yml +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/.gitignore +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/.scars/0001-git-grep-ere-pitfalls.landmine.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/.scars/0002-agent-direct-hook-install.deadend.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/.scars/0003-installer-binds-to-active-venv-scar.landmine.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/.scars/0004-promote-roundtrip-drops-expires-evidence.landmine.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/.scars/0005-history-rewrite-orphans-commit-evidence.landmine.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/.scars/0006-yaml-pattern-anchor-over-escaping.landmine.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/.scars/README.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/AGENTS.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/CONTRIBUTING.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/IDEA.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/LICENSE +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/README.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/ROADMAP.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/SCAR-FORMAT.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/SPEC.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/STRESS-TEST.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/anchor-survival/PROTOCOL.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/anchor-survival/RESULTS.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/anchor-survival/long_replay.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/anchor-survival/replay.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/auto-authorship/FINDINGS.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/auto-authorship/PROTOCOL.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/fence-honor/.gitignore +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/fence-honor/PROTOCOL.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/fence-honor/RESULTS.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/fence-honor/fixture/.scars/0001-vendor-retry-window.fence.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/fence-honor/fixture/.scars/0002-evicting-session-store.deadend.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/fence-honor/fixture/.scars/0003-export-column-order.landmine.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/fence-honor/fixture/README.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/fence-honor/fixture/payments/retry.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/fence-honor/fixture/reports/export.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/fence-honor/fixture/services/sessions.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/fence-honor/grade.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/harvest/PROTOCOL.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/experiments/harvest/harvest.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/hook/scar-hooks.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/plugin/skills/scar-authoring/SKILL.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/release-please-config.json +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/src/scar/__init__.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/src/scar/agent.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/src/scar/cli.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/src/scar/evidence.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/src/scar/hooks.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/src/scar/installer.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/src/scar/match.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/src/scar/mcp.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/src/scar/orphan.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/src/scar/render.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/src/scar/skills/scar-authoring/SKILL.md +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/tests/test_cli.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/tests/test_docs.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/tests/test_evidence.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/tests/test_hooks.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/tests/test_installer.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/tests/test_lifecycle.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/tests/test_match.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/tests/test_mcp.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/tests/test_orphan.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/tests/test_plugin.py +0 -0
- {scar_cli-0.6.1 → scar_cli-0.7.0}/tests/test_skill.py +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: 7
|
|
3
|
+
type: landmine
|
|
4
|
+
title: Changing release-please extra-files config does NOT retro-apply to an already-open release PR
|
|
5
|
+
severity: medium
|
|
6
|
+
confidence: 0.85
|
|
7
|
+
created: 2026-06-26
|
|
8
|
+
authors: ["claude-code", "Kibukx"]
|
|
9
|
+
anchors:
|
|
10
|
+
- path: release-please-config.json
|
|
11
|
+
- path: .github/workflows/release.yml
|
|
12
|
+
evidence:
|
|
13
|
+
- pr: 64
|
|
14
|
+
- pr: 66
|
|
15
|
+
- note: v0.6.1 release cycle, 2026-06-26
|
|
16
|
+
expires:
|
|
17
|
+
condition: "release-please gains retro-application of config changes to open release PRs, or the repo stops carrying version in extra files (plugin.json)"
|
|
18
|
+
review_after: 2027-06-26
|
|
19
|
+
status: active
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
If a release PR is already open and you then merge a change to `release-please-config.json`
|
|
23
|
+
(e.g. adding/altering `extra-files`), release-please will NOT retro-apply the new config to
|
|
24
|
+
that open PR — especially when the commit that lands the config is a non-releasable type
|
|
25
|
+
(`ci:`, `chore:`), because release-please sees no new releasable commit and leaves the
|
|
26
|
+
existing PR untouched.
|
|
27
|
+
|
|
28
|
+
Observed live: PR #64 (release 0.6.1) was computed from the #63 `fix:` commit BEFORE #65 added
|
|
29
|
+
the `extra-files` updater for `plugin/plugin.json`. When #65 (a `ci:` commit) merged,
|
|
30
|
+
release-please re-ran but did not re-bump the open PR, so #64 bumped `pyproject.toml` only.
|
|
31
|
+
`plugin/plugin.json` stayed behind and the `test_plugin_version_matches_pyproject` drift guard
|
|
32
|
+
failed CI — correctly blocking a broken release.
|
|
33
|
+
|
|
34
|
+
Fix when this happens: close the stale release PR, delete its `release-please--branches--main`
|
|
35
|
+
branch, and re-dispatch the Release workflow (`gh workflow run release.yml`). The fresh PR is
|
|
36
|
+
built from scratch and applies the current `extra-files` config. Do NOT hand-patch the version
|
|
37
|
+
into the open release branch — it leaves the manifest/state half-migrated. The drift guard is
|
|
38
|
+
the safety net: it makes this failure loud instead of shipping a mismatched manifest.
|
|
@@ -4,3 +4,4 @@
|
|
|
4
4
|
2026-06-13 — false trigger: signals from iterative re-anchoring + a caught staging bug (fixed #51) + gh CLI flakiness; all genuine lessons already captured as scars #5/#6 and issues #50
|
|
5
5
|
2026-06-13 false trigger: harvest-labeling session — 'revert/reverted/abandoned' prose describes ANOTHER repo's history (homelab-apps reverts judged as scar candidates), not an approach abandoned here; tool_errors were a wc-on-wrong-path + benign exit-1 during label recording. Curation work trips the drafter like meta-sessions do (FP #1).
|
|
6
6
|
2026-06-24 false trigger: #54 scorer investigation — 'revert' is feature-domain (the revert heuristic) + 'user_corrections'/'wrong' is me correcting the ISSUE BODY's mechanism claim (flapping base vs uncapped osc bonus), not abandoning a tried code approach. Read-only mapping + scope question; no code written, nothing deadended. Same self-ref class as FP #1/#5: sessions reasoning about the scorer trip the scorer.
|
|
7
|
+
2026-06-29 — FP: 'revert' was #54 harvest revert-signal discussion; corrections were process steering (wait for investigation, repeat question); no approach tried-and-abandoned.
|
|
@@ -13,7 +13,10 @@ anchors:
|
|
|
13
13
|
- path: src/module/ # file or directory this protects
|
|
14
14
|
- pattern: "regex" # optional: fires when matching code appears in ANY new/edited file
|
|
15
15
|
evidence:
|
|
16
|
-
-
|
|
16
|
+
- pr: 123 # at least one receipt: pr, issue, url, commit, incident, or note
|
|
17
|
+
# Prefer pr/issue/url over a bare commit: SHA — feature-branch SHAs orphan on squash-merge.
|
|
18
|
+
- issue: 50
|
|
19
|
+
- url: https://github.com/org/repo/commit/abc1234
|
|
17
20
|
expires:
|
|
18
21
|
condition: "what change would make this scar obsolete"
|
|
19
22
|
review_after: 1971-01-01 # force a freshness look even if condition never triggers
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.0](https://github.com/Daily-Nerd/Scar/compare/v0.6.1...v0.7.0) (2026-06-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* durable evidence forms (issue:/url:) to survive squash-merge ([#50](https://github.com/Daily-Nerd/Scar/issues/50)) ([#74](https://github.com/Daily-Nerd/Scar/issues/74)) ([d558849](https://github.com/Daily-Nerd/Scar/commit/d558849652c068a9679617c5ed522b3eefffcc78))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **harvest:** exclude vendored/scaffold paths from comment-archaeology ([#72](https://github.com/Daily-Nerd/Scar/issues/72)) ([#73](https://github.com/Daily-Nerd/Scar/issues/73)) ([919add4](https://github.com/Daily-Nerd/Scar/commit/919add4b3503b65f866274d97995088a0aa8612a))
|
|
14
|
+
* **parser:** strip unquoted inline comments in _field ([#70](https://github.com/Daily-Nerd/Scar/issues/70)) ([03ec578](https://github.com/Daily-Nerd/Scar/commit/03ec578290f8dd8ce4ed436367a12eabbdb5c120))
|
|
15
|
+
|
|
3
16
|
## [0.6.1](https://github.com/Daily-Nerd/Scar/compare/v0.6.0...v0.6.1) (2026-06-26)
|
|
4
17
|
|
|
5
18
|
|
|
@@ -13,7 +13,10 @@ anchors:
|
|
|
13
13
|
- path: src/module/ # file or directory this protects
|
|
14
14
|
- pattern: "regex" # optional: fires when matching code appears in ANY new/edited file
|
|
15
15
|
evidence:
|
|
16
|
-
-
|
|
16
|
+
- pr: 123 # at least one receipt: pr, issue, url, commit, incident, or note
|
|
17
|
+
# Prefer pr/issue/url over a bare commit: SHA — feature-branch SHAs orphan on squash-merge.
|
|
18
|
+
- issue: 50
|
|
19
|
+
- url: https://github.com/org/repo/commit/abc1234
|
|
17
20
|
expires:
|
|
18
21
|
condition: "what change would make this scar obsolete"
|
|
19
22
|
review_after: 1971-01-01 # force a freshness look even if condition never triggers
|
|
@@ -351,6 +351,18 @@ def _in_scars(path: str) -> bool:
|
|
|
351
351
|
return ".scars" in path.split("/")
|
|
352
352
|
|
|
353
353
|
|
|
354
|
+
# Vendored dependency trees + tool-scaffold dirs. Their files carry DO-NOT /
|
|
355
|
+
# load-bearing prose the project never wrote (third-party docs, generated
|
|
356
|
+
# config), so comment-archaeology self-matches the same way #55's .scars did.
|
|
357
|
+
# Matched as any path segment — a nested node_modules/ is the same noise (#72).
|
|
358
|
+
_VENDORED_SEGMENTS = frozenset({"node_modules", "vendor", ".tessl", ".kiro"})
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _is_vendored(path: str) -> bool:
|
|
362
|
+
"""True if a candidate path lives under a vendored/scaffold tree (#72)."""
|
|
363
|
+
return bool(_VENDORED_SEGMENTS & set(path.split("/")))
|
|
364
|
+
|
|
365
|
+
|
|
354
366
|
def _annotate_and_sort(candidates: list[dict], signal_type: str,
|
|
355
367
|
now_months: int | None = None) -> list[dict]:
|
|
356
368
|
"""Add 'score' and 'id' fields to each candidate, then sort by score desc."""
|
|
@@ -370,12 +382,16 @@ def harvest(repo: Path) -> dict[str, list[dict]]:
|
|
|
370
382
|
"flapping": _flapping(repo),
|
|
371
383
|
"comments": _comment_archaeology(repo),
|
|
372
384
|
}
|
|
373
|
-
# Drop a repo's own .scars/ tree
|
|
374
|
-
# (#
|
|
375
|
-
#
|
|
385
|
+
# Drop self-ref noise (#55: a repo's own .scars/ tree) and vendored/scaffold
|
|
386
|
+
# noise (#72: node_modules, vendor, tool-scaffold docs). Filtered in code,
|
|
387
|
+
# not via a git grep pathspec (landmine #1: a zero-match pathspec silently
|
|
388
|
+
# empties the whole result set).
|
|
389
|
+
def _excluded(section: str, c: dict) -> bool:
|
|
390
|
+
path = _candidate_path(_SECTION_TYPES[section], c)
|
|
391
|
+
return _in_scars(path) or _is_vendored(path)
|
|
392
|
+
|
|
376
393
|
raw = {
|
|
377
|
-
section: [c for c in candidates
|
|
378
|
-
if not _in_scars(_candidate_path(_SECTION_TYPES[section], c))]
|
|
394
|
+
section: [c for c in candidates if not _excluded(section, c)]
|
|
379
395
|
for section, candidates in raw.items()
|
|
380
396
|
}
|
|
381
397
|
return {
|
|
@@ -9,7 +9,7 @@ import re
|
|
|
9
9
|
import time
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
|
|
12
|
-
from .model import SEVERITIES, STATUSES, TYPES, ParseError, parse_scar_text
|
|
12
|
+
from .model import SEVERITIES, STATUSES, TYPES, ParseError, is_valid_url, parse_scar_text
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@dataclass
|
|
@@ -46,6 +46,11 @@ def lint_text(text: str, today: str | None = None) -> list[Finding]:
|
|
|
46
46
|
findings.append(Finding("error", f"invalid pattern anchor /{pat}/: {exc}"))
|
|
47
47
|
if not scar.evidence:
|
|
48
48
|
findings.append(Finding("warning", "no evidence links — challengeable on sight"))
|
|
49
|
+
for e in scar.evidence:
|
|
50
|
+
if e.startswith("url:") and not is_valid_url(e[len("url:"):]):
|
|
51
|
+
value = e[len("url:"):].strip()
|
|
52
|
+
findings.append(Finding(
|
|
53
|
+
"warning", f"url evidence not a valid http(s) link: '{value}'"))
|
|
49
54
|
# ISO dates compare correctly as strings; never an error — a human
|
|
50
55
|
# decides whether to archive (ADR-4), lint only surfaces the due date
|
|
51
56
|
if (scar.status in ("active", "challenged") and scar.review_after
|
|
@@ -16,6 +16,13 @@ SEVERITIES = ("low", "medium", "high", "critical")
|
|
|
16
16
|
STATUSES = ("candidate", "active", "challenged", "archived", "orphaned", "template")
|
|
17
17
|
|
|
18
18
|
_FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n?(.*)$", re.DOTALL)
|
|
19
|
+
_URL_RE = re.compile(r"^https?://")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_valid_url(value: str) -> bool:
|
|
23
|
+
"""True iff value is an http(s) URL. Used by lint to flag durable-link
|
|
24
|
+
evidence (url:) that isn't actually a link (#50)."""
|
|
25
|
+
return bool(_URL_RE.match(value.strip()))
|
|
19
26
|
|
|
20
27
|
|
|
21
28
|
class ParseError(ValueError):
|
|
@@ -67,7 +74,17 @@ class Scar:
|
|
|
67
74
|
|
|
68
75
|
def _field(front: str, name: str, default: str = "") -> str:
|
|
69
76
|
m = re.search(rf"^\s*{name}:\s*(.+?)\s*$", front, re.MULTILINE)
|
|
70
|
-
|
|
77
|
+
if not m:
|
|
78
|
+
return default
|
|
79
|
+
value = m.group(1)
|
|
80
|
+
# Strip an unquoted inline comment (YAML: a '#' after whitespace starts a
|
|
81
|
+
# comment). The template ships '# ...' on every field; a copied-but-uncleaned
|
|
82
|
+
# comment otherwise lands in the value — silently for confidence/severity,
|
|
83
|
+
# loudly for type. Quoted values are left intact so a '#' inside a quoted
|
|
84
|
+
# scalar (title, condition) stays data. See issue #69.
|
|
85
|
+
if '"' not in value and "'" not in value:
|
|
86
|
+
value = re.sub(r"\s+#.*$", "", value).rstrip()
|
|
87
|
+
return value
|
|
71
88
|
|
|
72
89
|
|
|
73
90
|
def parse_scar_text(text: str) -> Scar:
|
|
@@ -91,7 +108,7 @@ def parse_scar_text(text: str) -> Scar:
|
|
|
91
108
|
for a in authors_raw.strip("[]").split(",") if a.strip()] if authors_raw else []
|
|
92
109
|
|
|
93
110
|
evidence = [f"{m1.group(1)}: {m1.group(2).strip().strip('\"')}" for m1 in re.finditer(
|
|
94
|
-
r"^\s*-\s*(commit|pr|incident|note):\s*(.+)\s*$", front, re.MULTILINE)]
|
|
111
|
+
r"^\s*-\s*(commit|pr|issue|incident|note|url):\s*(.+)\s*$", front, re.MULTILINE)]
|
|
95
112
|
|
|
96
113
|
return Scar(
|
|
97
114
|
type=_field(front, "type", "deadend"),
|
|
@@ -13,7 +13,10 @@ anchors:
|
|
|
13
13
|
- path: src/module/ # file or directory this protects
|
|
14
14
|
- pattern: "regex" # optional: fires when matching code appears in ANY new/edited file
|
|
15
15
|
evidence:
|
|
16
|
-
-
|
|
16
|
+
- pr: 123 # at least one receipt: pr, issue, url, commit, incident, or note
|
|
17
|
+
# Prefer pr/issue/url over a bare commit: SHA — feature-branch SHAs orphan on squash-merge.
|
|
18
|
+
- issue: 50
|
|
19
|
+
- url: https://github.com/org/repo/commit/abc1234
|
|
17
20
|
expires:
|
|
18
21
|
condition: "what change would make this scar obsolete"
|
|
19
22
|
review_after: 1971-01-01 # force a freshness look even if condition never triggers
|
|
@@ -34,8 +34,9 @@ challenge it: update or archive it with a note, don't ignore it.
|
|
|
34
34
|
2. **YAML frontmatter is mandatory.** A scar without it is unparseable and
|
|
35
35
|
will NEVER fire in any tool. `scar lint` checks; the hooks warn loudly.
|
|
36
36
|
3. **Promotion** = human review: `scar promote candidates/<slug>.md`.
|
|
37
|
-
4. **Evidence required.** A scar without a commit/
|
|
38
|
-
an opinion and can be challenged on sight.
|
|
37
|
+
4. **Evidence required.** A scar without a pr/issue/url/commit/incident
|
|
38
|
+
reference is an opinion and can be challenged on sight. Prefer durable
|
|
39
|
+
pr/issue/url refs — feature-branch commit SHAs orphan on squash-merge.
|
|
39
40
|
|
|
40
41
|
Format details: `template.md`. Project: SCAR.
|
|
41
42
|
"""
|
|
@@ -56,7 +57,10 @@ anchors:
|
|
|
56
57
|
- path: src/module/ # file or directory this protects
|
|
57
58
|
- pattern: "regex" # optional: fires when matching code appears in ANY new/edited file
|
|
58
59
|
evidence:
|
|
59
|
-
-
|
|
60
|
+
- pr: 123 # at least one receipt: pr, issue, url, commit, incident, or note
|
|
61
|
+
# Prefer pr/issue/url over a bare commit: SHA — feature-branch SHAs orphan on squash-merge.
|
|
62
|
+
- issue: 50
|
|
63
|
+
- url: https://github.com/org/repo/commit/abc1234
|
|
60
64
|
expires:
|
|
61
65
|
condition: "what change would make this scar obsolete"
|
|
62
66
|
review_after: 1971-01-01
|
|
@@ -105,6 +105,49 @@ def test_excludes_scars_dir_from_candidates(tmp_path):
|
|
|
105
105
|
"no candidate may point into any .scars/ tree (self-ref noise, #55)"
|
|
106
106
|
|
|
107
107
|
|
|
108
|
+
def test_excludes_vendored_and_scaffold_dirs_from_candidates(tmp_path):
|
|
109
|
+
"""Scar #72: harvest must not surface candidates from vendored/scaffold trees.
|
|
110
|
+
|
|
111
|
+
Comment-archaeology greps DO-NOT/load-bearing prose across the whole repo,
|
|
112
|
+
so third-party doc tiles and dependency trees (node_modules/, vendor/, and
|
|
113
|
+
tool-scaffold dirs like .tessl/.kiro) match the fence regex with prose the
|
|
114
|
+
project never wrote. Same path-based pollution class as #55's .scars filter.
|
|
115
|
+
"""
|
|
116
|
+
git(tmp_path.parent, "init", "-q", "-b", "main", str(tmp_path))
|
|
117
|
+
git(tmp_path, "config", "user.email", "t@t")
|
|
118
|
+
git(tmp_path, "config", "user.name", "t")
|
|
119
|
+
# real code comment — SHOULD still be harvested
|
|
120
|
+
(tmp_path / "app.py").write_text(
|
|
121
|
+
"# DO NOT remove this init — load-bearing\nx = 1\n")
|
|
122
|
+
# vendored / scaffold trees full of trigger prose — must NOT be harvested
|
|
123
|
+
vendored = {
|
|
124
|
+
"node_modules/pkg/readme.md": "DO NOT edit; load-bearing vendored code.\n",
|
|
125
|
+
"vendor/lib/notes.md": "This must remain unchanged — load-bearing.\n",
|
|
126
|
+
".tessl/tiles/sqlalchemy/docs/dialects.md":
|
|
127
|
+
"Add ON CONFLICT DO NOTHING clause. DO NOT change.\n",
|
|
128
|
+
".kiro/steering/rules.md": "// DO NOT: forget the route guard\n",
|
|
129
|
+
}
|
|
130
|
+
for rel, content in vendored.items():
|
|
131
|
+
p = tmp_path / rel
|
|
132
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
p.write_text(content)
|
|
134
|
+
git(tmp_path, "add", "-A")
|
|
135
|
+
git(tmp_path, "commit", "-qm", "feat: code + vendored trees")
|
|
136
|
+
|
|
137
|
+
result = harvest(tmp_path)
|
|
138
|
+
locations = [c["location"] for c in result["comments"]]
|
|
139
|
+
assert any(loc.startswith("app.py") for loc in locations), \
|
|
140
|
+
"real code comment should still be harvested"
|
|
141
|
+
all_paths = (
|
|
142
|
+
locations
|
|
143
|
+
+ [c["component"] for c in result["deleted_components"]]
|
|
144
|
+
+ [c["file"] for c in result["flapping"]]
|
|
145
|
+
)
|
|
146
|
+
vendored_segs = {"node_modules", "vendor", ".tessl", ".kiro"}
|
|
147
|
+
assert not any(vendored_segs & set(p.split("/")) for p in all_paths), \
|
|
148
|
+
"no candidate may point into a vendored/scaffold tree (#72)"
|
|
149
|
+
|
|
150
|
+
|
|
108
151
|
# ---------------------------------------------------------------------------
|
|
109
152
|
# Ranking / scoring tests
|
|
110
153
|
# ---------------------------------------------------------------------------
|
|
@@ -68,3 +68,32 @@ def test_bad_pattern_regex_is_error():
|
|
|
68
68
|
' - pattern: "([unclosed"')
|
|
69
69
|
findings = lint_text(bad)
|
|
70
70
|
assert any(f.level == "error" and "pattern" in f.message for f in findings)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_malformed_url_evidence_is_warning():
|
|
74
|
+
bad = GOOD.replace(
|
|
75
|
+
"evidence:\n - commit: abc1234\n",
|
|
76
|
+
"evidence:\n - url: not-a-link\n",
|
|
77
|
+
)
|
|
78
|
+
findings = lint_text(bad)
|
|
79
|
+
assert any(f.level == "warning" and "url" in f.message and "not-a-link" in f.message
|
|
80
|
+
for f in findings)
|
|
81
|
+
assert not any(f.level == "error" for f in findings)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_valid_url_evidence_no_warning():
|
|
85
|
+
ok = GOOD.replace(
|
|
86
|
+
"evidence:\n - commit: abc1234\n",
|
|
87
|
+
"evidence:\n - url: https://github.com/org/repo/commit/abc1234\n",
|
|
88
|
+
)
|
|
89
|
+
findings = lint_text(ok)
|
|
90
|
+
assert not any("url" in f.message for f in findings)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_issue_evidence_skips_url_check():
|
|
94
|
+
ok = GOOD.replace(
|
|
95
|
+
"evidence:\n - commit: abc1234\n",
|
|
96
|
+
"evidence:\n - issue: 50\n",
|
|
97
|
+
)
|
|
98
|
+
findings = lint_text(ok)
|
|
99
|
+
assert findings == []
|
|
@@ -56,6 +56,31 @@ def test_confidence_defaults_when_malformed():
|
|
|
56
56
|
assert parse_scar_text(text).confidence == 0.5
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def test_inline_comment_stripped_from_type():
|
|
60
|
+
# The template ships `type: deadend # ...`; a copied-but-uncleaned comment
|
|
61
|
+
# must not become part of the value (else the type fails its enum check).
|
|
62
|
+
text = VALID.replace("type: deadend", "type: deadend # tried+failed")
|
|
63
|
+
assert parse_scar_text(text).type == "deadend"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_inline_comment_stripped_from_confidence_keeps_value():
|
|
67
|
+
# Silent-corruption case: a comment used to make float() fail -> reset to 0.5.
|
|
68
|
+
text = VALID.replace("confidence: 0.9", "confidence: 0.9 # 0..1 how sure")
|
|
69
|
+
assert parse_scar_text(text).confidence == 0.9
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_inline_comment_stripped_from_severity():
|
|
73
|
+
text = VALID.replace("severity: high", "severity: high # low | medium | high | critical")
|
|
74
|
+
assert parse_scar_text(text).severity == "high"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_quoted_hash_is_data_not_comment():
|
|
78
|
+
# A '#' inside a quoted scalar is part of the value, not a comment.
|
|
79
|
+
text = VALID.replace('condition: "sessions become re-derivable"',
|
|
80
|
+
'condition: "drop #legacy sessions"')
|
|
81
|
+
assert parse_scar_text(text).expires_condition == "drop #legacy sessions"
|
|
82
|
+
|
|
83
|
+
|
|
59
84
|
def test_quoted_pattern_anchor_unwrapped():
|
|
60
85
|
s = parse_scar_text(VALID)
|
|
61
86
|
assert '"' not in s.pattern_anchors[0]
|
|
@@ -98,3 +123,57 @@ Body.
|
|
|
98
123
|
assert 'inner' in s.evidence[0]
|
|
99
124
|
s2 = parse_scar_text(s.to_text())
|
|
100
125
|
assert s2.evidence == s.evidence
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_issue_and_url_evidence_parse_and_roundtrip():
|
|
129
|
+
text = """\
|
|
130
|
+
---
|
|
131
|
+
type: landmine
|
|
132
|
+
title: Durable evidence forms
|
|
133
|
+
severity: medium
|
|
134
|
+
confidence: 0.7
|
|
135
|
+
anchors:
|
|
136
|
+
- path: src/scar/
|
|
137
|
+
evidence:
|
|
138
|
+
- pr: 123
|
|
139
|
+
- issue: 50
|
|
140
|
+
- url: https://github.com/org/repo/commit/abc1234
|
|
141
|
+
status: active
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
Body.
|
|
145
|
+
"""
|
|
146
|
+
s = parse_scar_text(text)
|
|
147
|
+
assert s.evidence == [
|
|
148
|
+
"pr: 123",
|
|
149
|
+
"issue: 50",
|
|
150
|
+
"url: https://github.com/org/repo/commit/abc1234",
|
|
151
|
+
]
|
|
152
|
+
s2 = parse_scar_text(s.to_text())
|
|
153
|
+
assert s2.evidence == s.evidence
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_existing_evidence_prefixes_still_parse():
|
|
157
|
+
text = """\
|
|
158
|
+
---
|
|
159
|
+
type: deadend
|
|
160
|
+
title: keeps old forms
|
|
161
|
+
severity: low
|
|
162
|
+
confidence: 0.5
|
|
163
|
+
anchors:
|
|
164
|
+
- path: a/
|
|
165
|
+
evidence:
|
|
166
|
+
- commit: a3f9c21
|
|
167
|
+
- incident: 2024-prod-outage
|
|
168
|
+
- note: archived 2025-01-01: superseded
|
|
169
|
+
status: active
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
Body.
|
|
173
|
+
"""
|
|
174
|
+
s = parse_scar_text(text)
|
|
175
|
+
assert s.evidence == [
|
|
176
|
+
"commit: a3f9c21",
|
|
177
|
+
"incident: 2024-prod-outage",
|
|
178
|
+
"note: archived 2025-01-01: superseded",
|
|
179
|
+
]
|
|
@@ -107,3 +107,18 @@ def test_scars_for_path_is_bidirectional(repo):
|
|
|
107
107
|
assert store.scars_for_path("src/deep/file.py")
|
|
108
108
|
assert store.scars_for_path("") # repo root: every anchor is under it
|
|
109
109
|
assert not store.scars_for_path("docs/readme.md")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_template_documents_durable_evidence_forms():
|
|
113
|
+
from scar.store import TEMPLATE
|
|
114
|
+
assert "issue:" in TEMPLATE
|
|
115
|
+
assert "url:" in TEMPLATE
|
|
116
|
+
assert "squash" in TEMPLATE.lower()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_template_with_durable_forms_parses_clean():
|
|
120
|
+
from scar.store import TEMPLATE
|
|
121
|
+
from scar.model import parse_scar_text
|
|
122
|
+
text = TEMPLATE.replace("status: template", "status: active")
|
|
123
|
+
s = parse_scar_text(text)
|
|
124
|
+
assert any(e.startswith("issue:") or e.startswith("url:") for e in s.evidence)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scar_cli-0.6.1 → scar_cli-0.7.0}/.scars/0003-installer-binds-to-active-venv-scar.landmine.md
RENAMED
|
File without changes
|
{scar_cli-0.6.1 → scar_cli-0.7.0}/.scars/0004-promote-roundtrip-drops-expires-evidence.landmine.md
RENAMED
|
File without changes
|
{scar_cli-0.6.1 → scar_cli-0.7.0}/.scars/0005-history-rewrite-orphans-commit-evidence.landmine.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|