Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions docs/specs/2026-10-15-195-history-window-coverage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Truthful Git History Window Coverage

## Traceability

- Spec ID: 195-history-window-coverage
- Story: [Issue #195, defect 1](https://github.com/QoderAI/better-harness/issues/195)
- Status: Implemented

## Intent

Keep bounded hotspot evidence from claiming complete time-window coverage when
Git history has not been exhausted. Coverage is separate from ranking confidence
and from similarity between hotspot arrays.

## Acceptance Scenarios

- AC-1: For real commits aged 150/75/10/5/1 days, a cap of 3 preserves the
bounded counts [3,3,3], exposes additional history, marks the 90/180-day
windows capped, and makes history-window-coverage review rather than ready.
The 30-day window remains unknown: an older next commit cannot prove that
later unscanned commits have older timestamps. A cap of 10 yields [3,4,5]
with complete coverage and ready, even though ranking confidence can be low.
- AC-2: Naturally identical windows and history exactly equal to the cap remain
complete when Git has no additional commits. Neither equality nor reaching
the cap alone proves truncation. Only the requested number of commits
contributes hotspot statistics; the extra probe is not analyzed.
- AC-3: The range exposes whether more scoped commits exist, whether the
repository is shallow, and the minimum/maximum analyzed commit timestamps
(ISO strings, null for empty history). Each window adds coverage status and
reasons. Shallow history is unknown unless an omitted in-window commit proves
capping. Non-monotonic timestamps must never establish false completeness.
- AC-4: Unborn repositories retain valid empty history-profile results; their
coverage can be complete but the matrix requires observed scoped commits.
The evidence pack's existing HEAD requirement is outside this fix. Skipped history retains
stable window fields with skipped coverage and a review matrix result.
Custom positive window sizes use their own coverage, including a single
requested window, without requiring three default windows.
- AC-5: Coverage uses the same Git scope/pathspec as statistics. File ignores
and language filters do not erase commit-limit evidence. Existing output
fields and trend labels remain available; interpretation and canonical
guidance explicitly qualify capped/unknown/skipped windows. A zero-count
analyzed window describes the observed sample, not the absence of commits
in unscanned history; this also holds for non-monotonic timestamps.

## Non-Goals

- Other defects or follow-up comments in issue #195; changes to scoring,
project discovery, asset inventory, or existing PR #202.
- Fetching/deepening history, unbounded history scans, reconstructing omitted
hotspots, or proving that all later commits predate a time boundary.
- Removing fields, new dependencies, UI changes, release metadata, pushing,
publishing, creating a PR, or committing on the user's behalf.

## Plan and Tasks

1. Convert the supplied reproduction into behavior tests through the public
analyzeGitHistoryProfile and buildEvidencePack seams in
test/governance/core-change-watch.test.mjs; retain a real failing run.
2. In scripts/core-change-watch/git-history-profile.mjs, read at most cap + 1
commits with the existing scoped Git argv, retain only cap for analysis,
and inspect shallow state. The next commit can prove capping for a window;
otherwise any remaining or shallow history leaves completeness unknown.
3. In scripts/core-change-watch/evidence-pack.mjs, require complete coverage
and observed history for ready; expose coverage in matrix evidence and
preserve the no-history schema fallback with skipped status.
4. Update the canonical [Core Change Watch guidance](../../references/project-harness/core-change-watch.md),
regenerate the routing graph, run the focused, documentation, full test,
and package gates, then perform standards/spec and traceability review.

## Test and Review Evidence

- Baseline: upstream/main 840ff549; supplied real-Git reproduction failed again
on 2026-10-15 with truncated windows must not claim ready coverage.
- AC-1 red/green: `npx vitest run test/governance/core-change-watch.test.mjs
-t 'capped history windows'` first failed with ready instead of review, then
passed after the fix. The original reproduction also passed against the
new worktree's modules (only its import paths changed).
- AC-1 through AC-5: `npx vitest run test/governance/core-change-watch.test.mjs`
passed 34/34, including all seven new real-Git behavior regressions.
- AC-5 follow-up: independent review found that a capped/unknown zero-count
sample still claimed no commits landed in the window. The new real-Git
regression failed on that wording before the fix, then passed with observed
sample wording for unknown/0 and capped/0, while a full scan found one commit.
Incremental Standards and Spec reviews reported 0 new findings and confirmed
the P2 closed.
- Documentation: `node scripts/doc-link-graph/cli.mjs skills/better-harness`
regenerated an unchanged graph (39 files, 56 links);
`npx vitest run test/skills-docs/doc-link-graph.test.mjs` passed 8/8.
- Full suite: `npm test -- --maxWorkers=4` passed 1789 tests, with 7 skipped
(119 files passed, 1 skipped). The first default run failed two Node
permission tests because a temporary dependency symlink escaped the
worktree. With physical local dependencies, a second default run failed an
unchanged Skill watcher's 3-second deadline; its isolated suite passed 9/9.
These failures are retained in the local delivery logs, not counted as passes.
- Packaging: `npm run pack:verify` passed (747 npm entries, 1008 runtime ZIP
entries). `git diff --check` passed. Commands above ran on macOS through
`mise exec node@24.21.0 --`; Windows/Linux execution remains unverified.
- Independent read-only Standards and Spec AI reviews each reported 0 findings.
The over-engineering review found no actionable deletion. Review Readiness
confirmed this issue/spec/AC chain, five changed files, no generated graph
drift, no staged changes, no unrelated metadata, and no new commit.
- Risk: one extra numstat commit is bounded overhead, not hotspot evidence.
Unknown is intentionally conservative when a cap hides older traversal
entries; no monotonic timestamp assumption is made. Scope and shallow
restrictions describe accessible Git history, not remote completeness.
- Compatibility: additive coverage/range metadata; ready can become review.
Existing labels describe requested horizons, not independent corroboration.
No dependency or CLI flag changes. Revert the focused diff to recover.
- AI: Generated by Codex; independent AI review and local runtime receipts
will be recorded here. No human technical review or three-platform execution
is implied by local macOS checks.
24 changes: 23 additions & 1 deletion references/project-harness/core-change-watch.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,29 @@ segment; retain them only as bounded search leads.

`historyProfile.confidence` describes whether the inspected commit count and
time span are sufficient for hotspot ranking. It does not set final model or
report confidence. For a readiness claim based on history:
report confidence. Window completeness is separate:

- `range.hasMoreCommits` reports whether a scoped cap + 1 probe found more
history; the extra commit never contributes hotspot statistics. Reaching
the cap alone, or identical hotspot arrays, does not prove missing history.
- `range.oldestTimestamp` and `range.newestTimestamp` are the minimum/maximum
analyzed commit times, not a guarantee that all commits between them were
scanned. `range.shallow` records the local shallow-repository boundary.
- Each window's `coverage.status` is `complete` when accessible scoped history
was exhausted in a non-shallow repository, `capped` when the next omitted
commit is in the window, or `unknown` when remaining/shallow history prevents
proof of completeness. An older next commit cannot prove that later
unscanned commits are older: Git timestamps need not be monotonic.
- Coverage describes the Git scope/pathspec before file-ignore and language
filtering. `--no-history` emits `skipped` coverage and null range observations.
- `history-window-coverage` is `ready` only with observed scoped commits and
complete coverage for every requested window, including custom horizons.
Empty, skipped, capped, or unknown history requires `review`. Trend labels
name requested horizons; capped/unknown windows do not independently prove
sustained or legacy activity. Low ranking confidence can coexist with
complete coverage.

For a readiness claim based on history:

- name the inspected window, such as the latest 30 commits or last 90 days;
- open the commits or nearby specs, tests, release notes, or review artifacts
Expand Down
21 changes: 16 additions & 5 deletions scripts/core-change-watch/evidence-pack.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -395,10 +395,17 @@ function buildReviewMatrix(projectProfile, historyProfile, coreAnalysis, diffImp
),
matrixItem(
"history-window-coverage",
"History uses 30/90/180 style windows with confidence notes",
historyProfile.historyWindows.length >= 3 && historyProfile.confidence ? "ready" : "review",
{ windows: historyProfile.historyWindows.map((item) => item.days), confidence: historyProfile.confidence?.confidence ?? "unknown" },
"Low history confidence should downgrade hotspot certainty, not block source inspection.",
"Requested history windows have complete coverage with confidence notes",
historyProfile.range.analyzedCommits > 0
&& historyProfile.historyWindows.length > 0
&& historyProfile.historyWindows.every((item) => item.coverage.status === "complete")
&& historyProfile.confidence ? "ready" : "review",
{
windows: historyProfile.historyWindows.map((item) => item.days),
coverage: historyProfile.historyWindows.map((item) => ({ days: item.days, ...item.coverage })),
confidence: historyProfile.confidence?.confidence ?? "unknown",
},
"Capped, unknown, skipped, or empty history needs review. Ranking confidence is separate from coverage and does not block source inspection.",
),
matrixItem(
"large-repo-safety",
Expand Down Expand Up @@ -492,13 +499,17 @@ export async function buildEvidencePack(options = {}) {
days,
since: null,
analyzedCommits: 0,
coverage: { status: "skipped", reasons: ["History scan skipped by --no-history."] },
trendLabel: days <= 30 ? "recent-hotspots" : days <= 90 ? "sustained-hotspots" : "legacy-or-long-range-hotspots",
interpretation: "History scan skipped; this window is present for schema stability only.",
hotFiles: [],
supportingHotFiles: [],
hotPaths: [],
})),
range: { maxCommits: 0, analyzedCommits: 0, newest: null, oldest: null },
range: {
maxCommits: 0, analyzedCommits: 0, newest: null, oldest: null,
hasMoreCommits: null, shallow: null, newestTimestamp: null, oldestTimestamp: null,
},
}
: await analyzeGitHistoryProfile({
cwd: repoRoot,
Expand Down
28 changes: 22 additions & 6 deletions scripts/core-change-watch/git-history-profile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -137,20 +137,27 @@ function collectHistoryStats(commits, allowedLanguages, analysisScope) {
};
}

function historyWindows(commits, allowedLanguages, daysList, analysisScope) {
function historyWindows(commits, allowedLanguages, daysList, analysisScope, nextCommit, shallow) {
const nowSeconds = Math.floor(Date.now() / 1000);
return daysList.map((days) => {
const sinceSeconds = nowSeconds - days * 24 * 60 * 60;
const windowCommits = commits.filter((commit) => commit.timestamp >= sinceSeconds);
const stats = collectHistoryStats(windowCommits, allowedLanguages, analysisScope);
const trend = trendForWindow(days, windowCommits.length);
const reasons = [];
if (nextCommit) reasons.push("Additional scoped commits exist beyond maxCommits; unscanned timestamps may fall in this window.");
if (shallow) reasons.push("Shallow history cannot establish complete window coverage.");
const status = nextCommit?.timestamp >= sinceSeconds ? "capped" : reasons.length > 0 ? "unknown" : "complete";

return {
days,
since: new Date(sinceSeconds * 1000).toISOString(),
analyzedCommits: windowCommits.length,
coverage: { status, reasons },
trendLabel: trend.label,
interpretation: trend.interpretation,
interpretation: status === "complete"
? trend.interpretation
: `${trend.interpretation} Coverage is ${status}; this horizon does not establish hotspot persistence.`,
hotFiles: sortedHotFiles(stats.fileStats, 10),
supportingHotFiles: sortedHotFiles(stats.supportingFileStats, 10),
hotPaths: sortedCounts(stats.pathStats, 10).map((item) => ({
Expand All @@ -164,7 +171,7 @@ function historyWindows(commits, allowedLanguages, daysList, analysisScope) {
function trendForWindow(days, analyzedCommits) {
const horizon = days <= 30 ? "recent" : days <= 90 ? "sustained" : "long-range";
const label = days <= 30 ? "recent-hotspots" : days <= 90 ? "sustained-hotspots" : "legacy-or-long-range-hotspots";
const emptySuffix = analyzedCommits === 0 ? " No commits landed in this window, so treat it as missing evidence." : "";
const emptySuffix = analyzedCommits === 0 ? " No commits were observed in the analyzed sample for this window." : "";
return {
label,
interpretation: `${horizon} window for spotting ${label.replaceAll("-", " ")} from bounded git history.${emptySuffix}`,
Expand Down Expand Up @@ -231,7 +238,7 @@ export async function analyzeGitHistoryProfile(options = {}) {
try {
output = git(
repoRoot,
["log", `-${maxCommits}`, "--numstat", "--format=commit%x1f%H%x1f%ct%x1f%s", ...scopePathspecArgs(analysisScope)],
["log", `-${maxCommits + 1}`, "--numstat", "--format=commit%x1f%H%x1f%ct%x1f%s", ...scopePathspecArgs(analysisScope)],
{ timeout: 30_000 },
);
} catch (error) {
Expand All @@ -249,8 +256,13 @@ export async function analyzeGitHistoryProfile(options = {}) {
if (output === undefined) throw error;
}
const parsedCommits = parseLogWithNumstat(output, analysisScope);
const filteredHistory = applyCommitIgnores(parsedCommits, options.ignore);
// One extra commit proves whether the limit binds; it never contributes stats.
// An older next commit cannot prove completeness: Git timestamps need not be monotonic.
const nextCommit = parsedCommits[maxCommits];
const shallow = git(repoRoot, ["rev-parse", "--is-shallow-repository"]).trim() === "true";
const filteredHistory = applyCommitIgnores(parsedCommits.slice(0, maxCommits), options.ignore);
const commits = filteredHistory.commits;
const timestamps = commits.map((commit) => commit.timestamp);
const stats = collectHistoryStats(commits, allowedLanguages, analysisScope);
const maxHotFiles = positiveInt(options.maxHotFiles, 40);

Expand All @@ -266,9 +278,13 @@ export async function analyzeGitHistoryProfile(options = {}) {
analyzedCommits: commits.length,
newest: commits[0]?.shortSha ?? null,
oldest: commits.at(-1)?.shortSha ?? null,
hasMoreCommits: Boolean(nextCommit),
shallow,
newestTimestamp: timestamps.length > 0 ? new Date(Math.max(...timestamps) * 1000).toISOString() : null,
oldestTimestamp: timestamps.length > 0 ? new Date(Math.min(...timestamps) * 1000).toISOString() : null,
},
confidence: historyConfidence(commits, maxCommits),
historyWindows: historyWindows(commits, allowedLanguages, windowDays, analysisScope),
historyWindows: historyWindows(commits, allowedLanguages, windowDays, analysisScope, nextCommit, shallow),
languageTouchCounts: sortedCounts(stats.languageStats, 12).map((item) => ({
language: item.name,
touches: item.count,
Expand Down
Loading