@andresmassello/uscha 1.60.1 → 1.62.0
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/README.md +1 -1
- package/package.json +1 -1
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +84 -2
- package/uscha-kit/.claude/skills/uscha-mirador/SKILL.md +1 -1
- package/uscha-kit/.claude/skills/uscha-status/SKILL.md +6 -0
- package/uscha-kit/.claude-plugin/plugin.json +1 -1
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/README.md +24 -2
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/reports/junit/.origin-cases.json +1 -0
- package/uscha-kit/reports/junit/.specdrift-cases.json +1 -1
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +84 -2
- package/uscha-kit/skills/uscha-mirador/SKILL.md +1 -1
- package/uscha-kit/skills/uscha-status/SKILL.md +6 -0
- package/uscha-kit/uscha.config.json +1 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Requires **Python 3.8+** on the machine (the engine is Python stdlib — no pip
|
|
|
40
40
|
runtime dependencies). The npm package is a thin router; the canonical installer is
|
|
41
41
|
`uscha-kit/install-uscha.py`.
|
|
42
42
|
|
|
43
|
-
**Kit v1.
|
|
43
|
+
**Kit v1.62.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
|
|
44
44
|
[changelog](https://github.com/andresmassello/uscha/blob/main/uscha-kit/CHANGELOG.md)
|
|
45
45
|
(the per-release changelogs live in the repo, not in the npm tarball)
|
|
46
46
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andresmassello/uscha",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.62.0",
|
|
4
4
|
"description": "Spec-driven development for LLM coding agents: 9 skills + a stdlib evidence engine. Facts block, guesses advise; the human approves.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Andres Massello",
|
|
@@ -1858,6 +1858,58 @@ def cmd_init(args):
|
|
|
1858
1858
|
f"coverage_threshold={defaults.get('coverage_threshold')})")
|
|
1859
1859
|
|
|
1860
1860
|
|
|
1861
|
+
def _origin_label(origin):
|
|
1862
|
+
"""Render an origin for humans. An unmeasurable tree state reads `unknown`, never
|
|
1863
|
+
`clean` -- the whole reason the field distinguishes False from None."""
|
|
1864
|
+
o = origin or {}
|
|
1865
|
+
sha = (o.get("commit") or "")[:8] or "no-commit"
|
|
1866
|
+
dirty = o.get("dirty")
|
|
1867
|
+
state = "unknown" if dirty is None else ("dirty" if dirty else "clean")
|
|
1868
|
+
return "%s/%s" % (sha, state)
|
|
1869
|
+
|
|
1870
|
+
|
|
1871
|
+
def _evidence_origin(repo_path):
|
|
1872
|
+
"""WHERE the evidence came from: the commit it was measured at, and whether the tree
|
|
1873
|
+
was clean (ADR-007). The engine's freshness check compares file MTIMES, so until now a
|
|
1874
|
+
snapshot could say "tests green" without being able to say green AT WHAT -- provenance
|
|
1875
|
+
true of the tree, not of the commit that will merge.
|
|
1876
|
+
|
|
1877
|
+
Absence is NAMED, never guessed: no git, no repo, unreadable -> both None. `dirty is
|
|
1878
|
+
None` must never read as clean; that distinction is the entire point of recording it.
|
|
1879
|
+
Advisory only -- nothing here scores or blocks.
|
|
1880
|
+
|
|
1881
|
+
Untracked files COUNT as dirty (plain --porcelain): an untracked file the suite depends
|
|
1882
|
+
on is exactly the contamination this records, and treating untracked as invisible is a
|
|
1883
|
+
mistake this engine already paid for once (fast-path, 1.57.0)."""
|
|
1884
|
+
origin = {"commit": None, "dirty": None}
|
|
1885
|
+
|
|
1886
|
+
def _git(*args):
|
|
1887
|
+
# OSError covers BOTH ways this used to take the whole snapshot down: git absent
|
|
1888
|
+
# from PATH (FileNotFoundError) and a repo_path that does not exist or is a file
|
|
1889
|
+
# (NotADirectoryError). Every sibling measurement in _snapshot already tolerates a
|
|
1890
|
+
# missing path; provenance must not be the one field that can crash the command it
|
|
1891
|
+
# only annotates. Same posture as _spike_branch. (Both found by fresh review, both
|
|
1892
|
+
# reproduced: an unconfigured or not-yet-cloned repo path is ordinary, not exotic.)
|
|
1893
|
+
try:
|
|
1894
|
+
return subprocess.run(["git"] + list(args), cwd=repo_path,
|
|
1895
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
1896
|
+
errors="replace")
|
|
1897
|
+
except OSError:
|
|
1898
|
+
return None
|
|
1899
|
+
|
|
1900
|
+
rev = _git("rev-parse", "HEAD")
|
|
1901
|
+
if rev is not None and rev.returncode == 0 and rev.stdout.strip():
|
|
1902
|
+
origin["commit"] = rev.stdout.strip()
|
|
1903
|
+
# `-- .` scopes the answer to repo_path. Without it, a repo entry pointing at a
|
|
1904
|
+
# SUBDIRECTORY of a larger working tree reports the whole outer repo's state, so an
|
|
1905
|
+
# unrelated edit elsewhere would mark this repo's evidence dirty -- and the ADR would
|
|
1906
|
+
# be claiming a per-path fact the code did not deliver.
|
|
1907
|
+
st = _git("status", "--porcelain", "--", ".")
|
|
1908
|
+
if st is not None and st.returncode == 0:
|
|
1909
|
+
origin["dirty"] = bool(st.stdout.strip())
|
|
1910
|
+
return origin
|
|
1911
|
+
|
|
1912
|
+
|
|
1861
1913
|
def _snapshot(ledger, name):
|
|
1862
1914
|
node = _repo_node(ledger, name)
|
|
1863
1915
|
cfg = _repo_cfg(ledger, name) if name != "integration" else {"path": ".", "type": "maven"}
|
|
@@ -1868,6 +1920,7 @@ def _snapshot(ledger, name):
|
|
|
1868
1920
|
"coverage": coverage(path, rtype),
|
|
1869
1921
|
"tests": test_count(path, rtype),
|
|
1870
1922
|
"loc": count_loc(path, rtype),
|
|
1923
|
+
"origin": _evidence_origin(path),
|
|
1871
1924
|
}
|
|
1872
1925
|
node["snapshots"].append(snap)
|
|
1873
1926
|
return snap
|
|
@@ -1887,6 +1940,7 @@ def cmd_snapshot(args):
|
|
|
1887
1940
|
f"coverage={cov['pct']}% (found={cov['report_found']}), "
|
|
1888
1941
|
f"tests={tests['total']} (found={tests['report_found']}), "
|
|
1889
1942
|
f"freshness={freshness.get('status', 'unknown')}, "
|
|
1943
|
+
f"origin={_origin_label(snap.get('origin'))}, "
|
|
1890
1944
|
f"prod_loc={loc['prod_loc']}, test_loc={loc['test_loc']}")
|
|
1891
1945
|
if freshness.get("status") == "stale":
|
|
1892
1946
|
print(f" test evidence stale: {freshness.get('reason')}")
|
|
@@ -2972,7 +3026,7 @@ def _sd_governs(path):
|
|
|
2972
3026
|
return None
|
|
2973
3027
|
if not lines or lines[0].strip() != "---":
|
|
2974
3028
|
return None
|
|
2975
|
-
globs, in_governs = None, False
|
|
3029
|
+
globs, in_governs, explicit_empty = None, False, False
|
|
2976
3030
|
# scan runs to the CLOSING fence, not an arbitrary window -- a governs: key late in a
|
|
2977
3031
|
# long frontmatter block must not silently read as UNMAPPED (fresh-review finding).
|
|
2978
3032
|
for ln in lines[1:]:
|
|
@@ -2984,6 +3038,8 @@ def _sd_governs(path):
|
|
|
2984
3038
|
if rest.startswith("[") and rest.endswith("]"):
|
|
2985
3039
|
globs = [x.strip().strip("\x27\x22")
|
|
2986
3040
|
for x in rest[1:-1].split(",") if x.strip()]
|
|
3041
|
+
# only an INLINE [] is a declaration of "nothing to govern"
|
|
3042
|
+
explicit_empty = not globs
|
|
2987
3043
|
in_governs = False
|
|
2988
3044
|
elif rest:
|
|
2989
3045
|
# bare scalar (`governs: src/**`) -- a plausible authoring shorthand;
|
|
@@ -2997,6 +3053,11 @@ def _sd_governs(path):
|
|
|
2997
3053
|
globs.append(s[2:].strip().strip("\x27\x22"))
|
|
2998
3054
|
elif s and not ln.startswith((" ", "\t")):
|
|
2999
3055
|
in_governs = False
|
|
3056
|
+
if globs == [] and not explicit_empty:
|
|
3057
|
+
# a `governs:` key with nothing usable under it (a placeholder, a comment, a typo) is
|
|
3058
|
+
# an UNFINISHED declaration, not a statement that this spec governs nothing. Report it
|
|
3059
|
+
# as UNMAPPED, which is what it is (fresh-review finding).
|
|
3060
|
+
return None
|
|
3000
3061
|
return globs
|
|
3001
3062
|
|
|
3002
3063
|
|
|
@@ -3057,6 +3118,16 @@ def cmd_spec_drift(args):
|
|
|
3057
3118
|
row.update({"verdict": "UNMAPPED", "reason": "no governs: frontmatter"})
|
|
3058
3119
|
results.append(row)
|
|
3059
3120
|
continue
|
|
3121
|
+
if not governs:
|
|
3122
|
+
# An EXPLICIT empty list is a declaration, not an omission: this decision governs
|
|
3123
|
+
# no code and never will. Negative ADRs ("we are NOT doing X, and why") are a
|
|
3124
|
+
# documented practice in this kit, and reporting them UNMAPPED forever turns a
|
|
3125
|
+
# correct state into permanent noise -- which is how an advisory gets ignored.
|
|
3126
|
+
# Found by running spec-drift on this repo's own ADR-004.
|
|
3127
|
+
row.update({"verdict": "NO-CODE",
|
|
3128
|
+
"reason": "declares governs: [] -- a decision that governs no code"})
|
|
3129
|
+
results.append(row)
|
|
3130
|
+
continue
|
|
3060
3131
|
matched = []
|
|
3061
3132
|
pats = [_fp_glob_re(g) for g in governs]
|
|
3062
3133
|
for f in tracked:
|
|
@@ -3117,7 +3188,8 @@ def cmd_spec_drift(args):
|
|
|
3117
3188
|
print("SPEC-DRIFT %s (advisory, lag > %dd):" % (args.repo, lag_days))
|
|
3118
3189
|
if not results:
|
|
3119
3190
|
print(" no spec documents found (SPEC.md / docs/adr/*.md)")
|
|
3120
|
-
mark = {"SPEC_STALE": "!!", "CLEAN": "ok", "UNMAPPED": "--", "UNTRACKED": "--"
|
|
3191
|
+
mark = {"SPEC_STALE": "!!", "CLEAN": "ok", "UNMAPPED": "--", "UNTRACKED": "--",
|
|
3192
|
+
"NO-CODE": "ok"}
|
|
3121
3193
|
for r_ in results:
|
|
3122
3194
|
line = " %s %s: %s" % (mark.get(r_["verdict"], "??"), r_["file"],
|
|
3123
3195
|
r_["verdict"])
|
|
@@ -4319,6 +4391,16 @@ def cmd_dashboard(args):
|
|
|
4319
4391
|
for r in {e.get("repo") for e in ledger["fast_path"]}}
|
|
4320
4392
|
if ledger.get("spec_drift"):
|
|
4321
4393
|
out["spec_drift"] = ledger["spec_drift"]
|
|
4394
|
+
# evidence_origin: the latest snapshot's origin per repo, and ONLY when one exists --
|
|
4395
|
+
# a ledger predating ADR-007 keeps the exact prior schema (same conditional-key rule
|
|
4396
|
+
# fast_path and spec_drift already follow).
|
|
4397
|
+
_org = {}
|
|
4398
|
+
for _rn, _rnode in ledger["repos"].items():
|
|
4399
|
+
_snaps = _rnode.get("snapshots") or []
|
|
4400
|
+
if _snaps and _snaps[-1].get("origin"):
|
|
4401
|
+
_org[_rn] = _snaps[-1]["origin"]
|
|
4402
|
+
if _org:
|
|
4403
|
+
out["evidence_origin"] = _org
|
|
4322
4404
|
if getattr(args, "json", False):
|
|
4323
4405
|
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
4324
4406
|
return
|
|
@@ -71,7 +71,7 @@ than inventing a step. Keep the CONTENT in the conversation's language and the l
|
|
|
71
71
|
repo straight from the ledger, or null when none was requested. The template degrades when
|
|
72
72
|
absent, like every other field.
|
|
73
73
|
- **Spec-drift (ADR-005):** `dashboard --json` carries `spec_drift` — the latest advisory
|
|
74
|
-
run (per-document verdicts: SPEC_STALE / CLEAN / UNMAPPED / UNTRACKED) — only when a run
|
|
74
|
+
run (per-document verdicts: SPEC_STALE / CLEAN / UNMAPPED / UNTRACKED / NO-CODE) — only when a run
|
|
75
75
|
exists in the ledger; a virgin ledger keeps the exact prior schema. Advisory visibility of
|
|
76
76
|
the spec-maintenance tax, never readiness input.
|
|
77
77
|
- **Modes card:** the template draws one card for both modes — fast-path verdict chips per
|
|
@@ -90,6 +90,12 @@ Line guide:
|
|
|
90
90
|
block with the latest verdict per repo (`fast-path: ALLOW (intent...)` / `ESCALATED`). Absent
|
|
91
91
|
entries → no line at all: silence is honest when no mode was requested.
|
|
92
92
|
|
|
93
|
+
**Evidence origin (ADR-007):** if the latest snapshot was measured on a DIRTY tree, add ONE
|
|
94
|
+
line: `evidence: measured dirty at <sha8> - not from the commit alone`. Say nothing when the
|
|
95
|
+
tree was clean (the normal case needs no words) and nothing when it is `null` (unmeasurable,
|
|
96
|
+
and inventing a state is worse than silence). This never explains a blocked phase: it scores
|
|
97
|
+
nothing and gates nothing.
|
|
98
|
+
|
|
93
99
|
**Spec-drift (ADR-005):** if the ledger carries a `spec_drift` run, add ONE line:
|
|
94
100
|
`spec-drift: N stale / M docs (advisory)` — or `spec-drift: no drift measured` when zero are
|
|
95
101
|
stale. Always label it advisory; it never explains a blocked phase. Absent key → no line.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "uscha",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.62.0",
|
|
5
5
|
"displayName": "Uscha",
|
|
6
6
|
"description": "Spec-driven development for LLM coding agents: 9 skills (discovery, adr-refine, reverse-discovery, characterize, devloop, sysdoc, rubric, mirador, status) + a stdlib measurement engine (qa_ledger.py, 32 subcommands + universal installer + npm/npx router). Facts block, guesses advise; the human approves.",
|
|
7
7
|
"author": {
|
package/uscha-kit/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# uscha-kit
|
|
2
2
|
|
|
3
|
-
**Kit version:** v1.
|
|
3
|
+
**Kit version:** v1.62.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
|
|
4
4
|
|
|
5
5
|
Spec-driven orchestrator + multi-repo QA for Claude Code, with a deterministic ledger.
|
|
6
6
|
**Nine skills** (`uscha-discovery`, `uscha-adr-refine`, `uscha-devloop`, `uscha-sysdoc`, `uscha-reverse-discovery`,
|
|
@@ -123,10 +123,32 @@ Per document: **`SPEC_STALE`** when governed code outran the spec by more than
|
|
|
123
123
|
`defaults.spec_drift.max_lag_days` (default 30), listing the newer files; **`CLEAN`** when it
|
|
124
124
|
did not; **`UNMAPPED`** when there is no `governs:` frontmatter *or its globs match nothing*
|
|
125
125
|
— absence of a mapping is absence of measurement, not "no drift"; **`UNTRACKED`** when the
|
|
126
|
-
spec has no commit date to compare
|
|
126
|
+
spec has no commit date to compare; **`NO-CODE`** when it declares `governs: []`, i.e. a
|
|
127
|
+
decision that governs no source (negative ADRs) — a declaration, not an omission. The latest run lands in the ledger (`spec_drift`) so the
|
|
127
128
|
mirador can surface it. No readiness impact, no exit-code gate: a stale spec is a prompt for
|
|
128
129
|
a human conversation, not a blocked pipeline.
|
|
129
130
|
|
|
131
|
+
## Evidence origin (ADR-007) - green, but green at *what*?
|
|
132
|
+
|
|
133
|
+
Freshness compares file mtimes, so the ledger could say "tests green" without being able to
|
|
134
|
+
say which commit that was true of. Every snapshot now stamps where it came from:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
"origin": { "commit": "5d17cf4...", "dirty": false }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Measured with `git rev-parse HEAD` and `git status --porcelain` in the repo path. **Untracked
|
|
141
|
+
files count as dirty** - an untracked file the suite depends on is exactly the contamination
|
|
142
|
+
worth recording. **No git, no repo -> both `null`**, and `dirty: null` never reads as clean:
|
|
143
|
+
a tree state nobody could measure is not a clean one.
|
|
144
|
+
|
|
145
|
+
Advisory throughout: `snapshot` prints `origin=<sha8>/<clean|dirty|unknown>`,
|
|
146
|
+
`dashboard --json` carries `evidence_origin` when a snapshot has one, `/uscha-status` says one
|
|
147
|
+
line when the latest evidence was dirty. Readiness, phase and convergence are untouched -
|
|
148
|
+
knowing a tree was dirty does not tell you the evidence is wrong, only that it was not
|
|
149
|
+
produced from a commit alone. The git-worktree clean-room that would answer the stronger
|
|
150
|
+
question is deliberately deferred; ADR-007 records why.
|
|
151
|
+
|
|
130
152
|
## End-to-end flow
|
|
131
153
|
|
|
132
154
|
`uscha-discovery` is the front for something new (you only have the idea); `uscha-adr-refine` is the front
|
package/uscha-kit/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
uscha-kit 1.
|
|
1
|
+
uscha-kit 1.62.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"AC-EP-01": true, "AC-EP-02": true, "AC-EP-03": true, "AC-EP-05": true, "AC-EP-04": true}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"AC-SD-01": true, "AC-SD-03": true, "AC-SD-02": true, "AC-SD-04": true}
|
|
1
|
+
{"AC-SD-01": true, "AC-SD-03": true, "AC-SD-05": true, "AC-SD-02": true, "AC-SD-04": true}
|
|
@@ -1858,6 +1858,58 @@ def cmd_init(args):
|
|
|
1858
1858
|
f"coverage_threshold={defaults.get('coverage_threshold')})")
|
|
1859
1859
|
|
|
1860
1860
|
|
|
1861
|
+
def _origin_label(origin):
|
|
1862
|
+
"""Render an origin for humans. An unmeasurable tree state reads `unknown`, never
|
|
1863
|
+
`clean` -- the whole reason the field distinguishes False from None."""
|
|
1864
|
+
o = origin or {}
|
|
1865
|
+
sha = (o.get("commit") or "")[:8] or "no-commit"
|
|
1866
|
+
dirty = o.get("dirty")
|
|
1867
|
+
state = "unknown" if dirty is None else ("dirty" if dirty else "clean")
|
|
1868
|
+
return "%s/%s" % (sha, state)
|
|
1869
|
+
|
|
1870
|
+
|
|
1871
|
+
def _evidence_origin(repo_path):
|
|
1872
|
+
"""WHERE the evidence came from: the commit it was measured at, and whether the tree
|
|
1873
|
+
was clean (ADR-007). The engine's freshness check compares file MTIMES, so until now a
|
|
1874
|
+
snapshot could say "tests green" without being able to say green AT WHAT -- provenance
|
|
1875
|
+
true of the tree, not of the commit that will merge.
|
|
1876
|
+
|
|
1877
|
+
Absence is NAMED, never guessed: no git, no repo, unreadable -> both None. `dirty is
|
|
1878
|
+
None` must never read as clean; that distinction is the entire point of recording it.
|
|
1879
|
+
Advisory only -- nothing here scores or blocks.
|
|
1880
|
+
|
|
1881
|
+
Untracked files COUNT as dirty (plain --porcelain): an untracked file the suite depends
|
|
1882
|
+
on is exactly the contamination this records, and treating untracked as invisible is a
|
|
1883
|
+
mistake this engine already paid for once (fast-path, 1.57.0)."""
|
|
1884
|
+
origin = {"commit": None, "dirty": None}
|
|
1885
|
+
|
|
1886
|
+
def _git(*args):
|
|
1887
|
+
# OSError covers BOTH ways this used to take the whole snapshot down: git absent
|
|
1888
|
+
# from PATH (FileNotFoundError) and a repo_path that does not exist or is a file
|
|
1889
|
+
# (NotADirectoryError). Every sibling measurement in _snapshot already tolerates a
|
|
1890
|
+
# missing path; provenance must not be the one field that can crash the command it
|
|
1891
|
+
# only annotates. Same posture as _spike_branch. (Both found by fresh review, both
|
|
1892
|
+
# reproduced: an unconfigured or not-yet-cloned repo path is ordinary, not exotic.)
|
|
1893
|
+
try:
|
|
1894
|
+
return subprocess.run(["git"] + list(args), cwd=repo_path,
|
|
1895
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
1896
|
+
errors="replace")
|
|
1897
|
+
except OSError:
|
|
1898
|
+
return None
|
|
1899
|
+
|
|
1900
|
+
rev = _git("rev-parse", "HEAD")
|
|
1901
|
+
if rev is not None and rev.returncode == 0 and rev.stdout.strip():
|
|
1902
|
+
origin["commit"] = rev.stdout.strip()
|
|
1903
|
+
# `-- .` scopes the answer to repo_path. Without it, a repo entry pointing at a
|
|
1904
|
+
# SUBDIRECTORY of a larger working tree reports the whole outer repo's state, so an
|
|
1905
|
+
# unrelated edit elsewhere would mark this repo's evidence dirty -- and the ADR would
|
|
1906
|
+
# be claiming a per-path fact the code did not deliver.
|
|
1907
|
+
st = _git("status", "--porcelain", "--", ".")
|
|
1908
|
+
if st is not None and st.returncode == 0:
|
|
1909
|
+
origin["dirty"] = bool(st.stdout.strip())
|
|
1910
|
+
return origin
|
|
1911
|
+
|
|
1912
|
+
|
|
1861
1913
|
def _snapshot(ledger, name):
|
|
1862
1914
|
node = _repo_node(ledger, name)
|
|
1863
1915
|
cfg = _repo_cfg(ledger, name) if name != "integration" else {"path": ".", "type": "maven"}
|
|
@@ -1868,6 +1920,7 @@ def _snapshot(ledger, name):
|
|
|
1868
1920
|
"coverage": coverage(path, rtype),
|
|
1869
1921
|
"tests": test_count(path, rtype),
|
|
1870
1922
|
"loc": count_loc(path, rtype),
|
|
1923
|
+
"origin": _evidence_origin(path),
|
|
1871
1924
|
}
|
|
1872
1925
|
node["snapshots"].append(snap)
|
|
1873
1926
|
return snap
|
|
@@ -1887,6 +1940,7 @@ def cmd_snapshot(args):
|
|
|
1887
1940
|
f"coverage={cov['pct']}% (found={cov['report_found']}), "
|
|
1888
1941
|
f"tests={tests['total']} (found={tests['report_found']}), "
|
|
1889
1942
|
f"freshness={freshness.get('status', 'unknown')}, "
|
|
1943
|
+
f"origin={_origin_label(snap.get('origin'))}, "
|
|
1890
1944
|
f"prod_loc={loc['prod_loc']}, test_loc={loc['test_loc']}")
|
|
1891
1945
|
if freshness.get("status") == "stale":
|
|
1892
1946
|
print(f" test evidence stale: {freshness.get('reason')}")
|
|
@@ -2972,7 +3026,7 @@ def _sd_governs(path):
|
|
|
2972
3026
|
return None
|
|
2973
3027
|
if not lines or lines[0].strip() != "---":
|
|
2974
3028
|
return None
|
|
2975
|
-
globs, in_governs = None, False
|
|
3029
|
+
globs, in_governs, explicit_empty = None, False, False
|
|
2976
3030
|
# scan runs to the CLOSING fence, not an arbitrary window -- a governs: key late in a
|
|
2977
3031
|
# long frontmatter block must not silently read as UNMAPPED (fresh-review finding).
|
|
2978
3032
|
for ln in lines[1:]:
|
|
@@ -2984,6 +3038,8 @@ def _sd_governs(path):
|
|
|
2984
3038
|
if rest.startswith("[") and rest.endswith("]"):
|
|
2985
3039
|
globs = [x.strip().strip("\x27\x22")
|
|
2986
3040
|
for x in rest[1:-1].split(",") if x.strip()]
|
|
3041
|
+
# only an INLINE [] is a declaration of "nothing to govern"
|
|
3042
|
+
explicit_empty = not globs
|
|
2987
3043
|
in_governs = False
|
|
2988
3044
|
elif rest:
|
|
2989
3045
|
# bare scalar (`governs: src/**`) -- a plausible authoring shorthand;
|
|
@@ -2997,6 +3053,11 @@ def _sd_governs(path):
|
|
|
2997
3053
|
globs.append(s[2:].strip().strip("\x27\x22"))
|
|
2998
3054
|
elif s and not ln.startswith((" ", "\t")):
|
|
2999
3055
|
in_governs = False
|
|
3056
|
+
if globs == [] and not explicit_empty:
|
|
3057
|
+
# a `governs:` key with nothing usable under it (a placeholder, a comment, a typo) is
|
|
3058
|
+
# an UNFINISHED declaration, not a statement that this spec governs nothing. Report it
|
|
3059
|
+
# as UNMAPPED, which is what it is (fresh-review finding).
|
|
3060
|
+
return None
|
|
3000
3061
|
return globs
|
|
3001
3062
|
|
|
3002
3063
|
|
|
@@ -3057,6 +3118,16 @@ def cmd_spec_drift(args):
|
|
|
3057
3118
|
row.update({"verdict": "UNMAPPED", "reason": "no governs: frontmatter"})
|
|
3058
3119
|
results.append(row)
|
|
3059
3120
|
continue
|
|
3121
|
+
if not governs:
|
|
3122
|
+
# An EXPLICIT empty list is a declaration, not an omission: this decision governs
|
|
3123
|
+
# no code and never will. Negative ADRs ("we are NOT doing X, and why") are a
|
|
3124
|
+
# documented practice in this kit, and reporting them UNMAPPED forever turns a
|
|
3125
|
+
# correct state into permanent noise -- which is how an advisory gets ignored.
|
|
3126
|
+
# Found by running spec-drift on this repo's own ADR-004.
|
|
3127
|
+
row.update({"verdict": "NO-CODE",
|
|
3128
|
+
"reason": "declares governs: [] -- a decision that governs no code"})
|
|
3129
|
+
results.append(row)
|
|
3130
|
+
continue
|
|
3060
3131
|
matched = []
|
|
3061
3132
|
pats = [_fp_glob_re(g) for g in governs]
|
|
3062
3133
|
for f in tracked:
|
|
@@ -3117,7 +3188,8 @@ def cmd_spec_drift(args):
|
|
|
3117
3188
|
print("SPEC-DRIFT %s (advisory, lag > %dd):" % (args.repo, lag_days))
|
|
3118
3189
|
if not results:
|
|
3119
3190
|
print(" no spec documents found (SPEC.md / docs/adr/*.md)")
|
|
3120
|
-
mark = {"SPEC_STALE": "!!", "CLEAN": "ok", "UNMAPPED": "--", "UNTRACKED": "--"
|
|
3191
|
+
mark = {"SPEC_STALE": "!!", "CLEAN": "ok", "UNMAPPED": "--", "UNTRACKED": "--",
|
|
3192
|
+
"NO-CODE": "ok"}
|
|
3121
3193
|
for r_ in results:
|
|
3122
3194
|
line = " %s %s: %s" % (mark.get(r_["verdict"], "??"), r_["file"],
|
|
3123
3195
|
r_["verdict"])
|
|
@@ -4319,6 +4391,16 @@ def cmd_dashboard(args):
|
|
|
4319
4391
|
for r in {e.get("repo") for e in ledger["fast_path"]}}
|
|
4320
4392
|
if ledger.get("spec_drift"):
|
|
4321
4393
|
out["spec_drift"] = ledger["spec_drift"]
|
|
4394
|
+
# evidence_origin: the latest snapshot's origin per repo, and ONLY when one exists --
|
|
4395
|
+
# a ledger predating ADR-007 keeps the exact prior schema (same conditional-key rule
|
|
4396
|
+
# fast_path and spec_drift already follow).
|
|
4397
|
+
_org = {}
|
|
4398
|
+
for _rn, _rnode in ledger["repos"].items():
|
|
4399
|
+
_snaps = _rnode.get("snapshots") or []
|
|
4400
|
+
if _snaps and _snaps[-1].get("origin"):
|
|
4401
|
+
_org[_rn] = _snaps[-1]["origin"]
|
|
4402
|
+
if _org:
|
|
4403
|
+
out["evidence_origin"] = _org
|
|
4322
4404
|
if getattr(args, "json", False):
|
|
4323
4405
|
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
4324
4406
|
return
|
|
@@ -71,7 +71,7 @@ than inventing a step. Keep the CONTENT in the conversation's language and the l
|
|
|
71
71
|
repo straight from the ledger, or null when none was requested. The template degrades when
|
|
72
72
|
absent, like every other field.
|
|
73
73
|
- **Spec-drift (ADR-005):** `dashboard --json` carries `spec_drift` — the latest advisory
|
|
74
|
-
run (per-document verdicts: SPEC_STALE / CLEAN / UNMAPPED / UNTRACKED) — only when a run
|
|
74
|
+
run (per-document verdicts: SPEC_STALE / CLEAN / UNMAPPED / UNTRACKED / NO-CODE) — only when a run
|
|
75
75
|
exists in the ledger; a virgin ledger keeps the exact prior schema. Advisory visibility of
|
|
76
76
|
the spec-maintenance tax, never readiness input.
|
|
77
77
|
- **Modes card:** the template draws one card for both modes — fast-path verdict chips per
|
|
@@ -90,6 +90,12 @@ Line guide:
|
|
|
90
90
|
block with the latest verdict per repo (`fast-path: ALLOW (intent...)` / `ESCALATED`). Absent
|
|
91
91
|
entries → no line at all: silence is honest when no mode was requested.
|
|
92
92
|
|
|
93
|
+
**Evidence origin (ADR-007):** if the latest snapshot was measured on a DIRTY tree, add ONE
|
|
94
|
+
line: `evidence: measured dirty at <sha8> - not from the commit alone`. Say nothing when the
|
|
95
|
+
tree was clean (the normal case needs no words) and nothing when it is `null` (unmeasurable,
|
|
96
|
+
and inventing a state is worse than silence). This never explains a blocked phase: it scores
|
|
97
|
+
nothing and gates nothing.
|
|
98
|
+
|
|
93
99
|
**Spec-drift (ADR-005):** if the ledger carries a `spec_drift` run, add ONE line:
|
|
94
100
|
`spec-drift: N stale / M docs (advisory)` — or `spec-drift: no drift measured` when zero are
|
|
95
101
|
stale. Always label it advisory; it never explains a blocked phase. Absent key → no line.
|