diff --git a/CHANGELOG.md b/CHANGELOG.md index 440b557..702b0de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Each entry links the GitHub issue (the canonical spec) and the merge PR (the shi ### Changed +- **Doctor advisory checks and dead lib.js readers deleted** — `backlog-doctor.js` drops `in_flight_staleness` (`--stale-days`) and `context_bloat`; both re-derive from `status.sh --json` `age_days` and `wc -l` and nothing consumed them. `lib.js` drops `parseMarkerMonth` and the triage config readers orphaned by #433 (259 lines with tests). The three guard checks and their JSON shapes are unchanged. Closes [#458](https://github.com/sungjunlee/dev-backlog/issues/458). - **status.sh trailing sections deleted** — the Local Files block (counted `tasks/*.md` mirrors by a Backlog.md `status:` field, a format parked at `v0.11.0`), the Relay Runs block (re-derived another tool's run-directory slug and grepped its manifests), and the Past sprints line are gone (54 lines with their smoke fixture); each was one command away and nothing consumed them. Output now ends after the Active Sprint block; `--json` and `--track` are unchanged. Closes [#459](https://github.com/sungjunlee/dev-backlog/issues/459). ## [0.12.0] — 2026-09-17 diff --git a/skills/dev-backlog/scripts/backlog-doctor.js b/skills/dev-backlog/scripts/backlog-doctor.js index d659301..2f01ba4 100644 --- a/skills/dev-backlog/scripts/backlog-doctor.js +++ b/skills/dev-backlog/scripts/backlog-doctor.js @@ -2,8 +2,8 @@ /** * Aggregate deterministic dev-backlog health checks into one CI-ready probe. * Each check normalizes to one verdict: active-sprint invariant, sprint shape, - * in-flight trace/staleness, _context.md bloat. Reassess is a human call at - * sprint close, not a counter (charter rev 19, #446). + * in-flight trace. Reassess is a human call at sprint close, not a counter + * (charter rev 19, #446). */ const fs = require("fs"); @@ -24,23 +24,19 @@ const { const { repoDisplayPath } = require("./portable-path.js"); const SCHEMA_VERSION = 1; -const DEFAULT_STALE_DAYS = 7; -const CONTEXT_BLOAT_LINE_THRESHOLD = 200; const REQUIRED_ACTIVE_SECTIONS = ["Goal", "Plan", "Running Context", "Progress"]; function usage() { return [ - "Usage: backlog-doctor.js [--json] [--stale-days N] [backlog-dir]", + "Usage: backlog-doctor.js [--json] [backlog-dir]", "", - "Runs active-sprint, sprint-shape, in-flight trace/staleness,", - "and _context.md bloat checks.", + "Runs active-sprint, sprint-shape, and in-flight trace checks.", ].join("\n"); } function parseArgs(args) { const options = { backlogDir: DEFAULT_BACKLOG_DIR, - staleDays: DEFAULT_STALE_DAYS, json: false, }; let backlogDirSet = false; @@ -52,21 +48,6 @@ function parseArgs(args) { options.json = true; continue; } - if (arg === "--stale-days") { - const next = args[i + 1]; - if (!next) return { ...options, error: `Missing value for --stale-days. ${usage()}` }; - const parsed = parseStaleDays(next); - if (parsed.error) return { ...options, error: parsed.error }; - options.staleDays = parsed.value; - i += 1; - continue; - } - if (arg.startsWith("--stale-days=")) { - const parsed = parseStaleDays(arg.slice("--stale-days=".length)); - if (parsed.error) return { ...options, error: parsed.error }; - options.staleDays = parsed.value; - continue; - } if (arg.startsWith("--")) { return { ...options, error: `Unknown argument: ${arg}. ${usage()}` }; } @@ -80,19 +61,10 @@ function parseArgs(args) { return options; } -function parseStaleDays(raw) { - if (!/^\d+$/.test(raw)) { - return { error: `Invalid --stale-days value: ${raw}. Expected a non-negative integer.` }; - } - return { value: Number.parseInt(raw, 10) }; -} - function runDoctor({ repoRoot = process.cwd(), backlogDir = DEFAULT_BACKLOG_DIR, - staleDays = DEFAULT_STALE_DAYS, today = new Date(), - contextLineThreshold = CONTEXT_BLOAT_LINE_THRESHOLD, } = {}) { const root = path.resolve(repoRoot); const backlogPath = resolvePath(root, backlogDir); @@ -115,10 +87,6 @@ function runDoctor({ tag, ), in_flight_trace: tagTrack(checkInFlightTrace({ sprintState, activeStatus: active.status }), tag), - in_flight_staleness: tagTrack( - checkInFlightStaleness({ sprintState, activeStatus: active.status, staleDays }), - tag, - ), }; }); @@ -126,8 +94,6 @@ function runDoctor({ active, ...perTrack.map((run) => run.sprint_shape), ...perTrack.map((run) => run.in_flight_trace), - ...perTrack.map((run) => run.in_flight_staleness), - checkContextBloat({ repoRoot: root, sprintsDir, threshold: contextLineThreshold }), ]; return { @@ -348,71 +314,6 @@ function checkInFlightTrace({ sprintState, activeStatus }) { }); } -function checkInFlightStaleness({ sprintState, activeStatus, staleDays }) { - if (sprintState.error) { - return verdict("in_flight_staleness", activeStatus === "fail" ? "warn" : "fail", { - summary: `Skipped in-flight staleness check: ${sprintState.error.message}`, - stale_days: staleDays, - }); - } - if (!sprintState.state || !sprintState.state.active_sprint) { - return verdict("in_flight_staleness", "pass", { - summary: "No active sprint; no in-flight age to check.", - stale_days: staleDays, - }); - } - - const stale = sprintState.state.in_flight - .filter((item) => item.age_days !== null && item.age_days > staleDays); - if (stale.length > 0) { - return verdict("in_flight_staleness", "warn", { - summary: `${stale.length} in-flight item(s) are older than ${staleDays} day(s).`, - stale_days: staleDays, - items: stale.map(publicPlanItem), - }); - } - - return verdict("in_flight_staleness", "pass", { - summary: `No in-flight items are older than ${staleDays} day(s).`, - stale_days: staleDays, - in_flight_count: sprintState.state.in_flight.length, - }); -} - -function checkContextBloat({ repoRoot, sprintsDir, threshold }) { - const contextPath = path.join(sprintsDir, "_context.md"); - if (!fs.existsSync(contextPath)) { - return verdict("context_bloat", "pass", { - summary: "_context.md is absent; no cross-sprint context bloat detected.", - threshold_lines: threshold, - line_count: 0, - }); - } - - const content = fs.readFileSync(contextPath, "utf-8"); - const lineCount = countLines(content); - if (lineCount > threshold) { - return verdict("context_bloat", "warn", { - summary: `_context.md is ${lineCount} lines, above the ${threshold}-line bloat threshold.`, - context_path: displayPath(repoRoot, contextPath), - threshold_lines: threshold, - line_count: lineCount, - }); - } - - return verdict("context_bloat", "pass", { - summary: `_context.md is within the ${threshold}-line bloat threshold.`, - context_path: displayPath(repoRoot, contextPath), - threshold_lines: threshold, - line_count: lineCount, - }); -} - -function countLines(content) { - if (content === "") return 0; - return content.split(/\r?\n/).length; -} - // Whitelist (order-stable) of the plan-item fields a verdict may publish. const PUBLIC_PLAN_FIELDS = [ "line", "tracker", "id", "ref", "issue_number", @@ -477,8 +378,6 @@ if (require.main === module) main(); module.exports = { SCHEMA_VERSION, DEFAULT_BACKLOG_DIR, - DEFAULT_STALE_DAYS, - CONTEXT_BLOAT_LINE_THRESHOLD, REQUIRED_ACTIVE_SECTIONS, parseArgs, runDoctor, diff --git a/skills/dev-backlog/scripts/lib.js b/skills/dev-backlog/scripts/lib.js index 2ea1e21..5218a14 100644 --- a/skills/dev-backlog/scripts/lib.js +++ b/skills/dev-backlog/scripts/lib.js @@ -1,9 +1,5 @@ -/** - * Shared library for dev-backlog Node scripts. - */ +/** Shared library for dev-backlog Node scripts. */ -const fs = require("fs"); -const path = require("path"); const { DEFAULT_BACKLOG_DIR } = require("./execution-root.js"); const GH_EXEC_DEFAULTS = Object.freeze({ @@ -49,11 +45,6 @@ function containsIssueRef(text, identity) { return false; } -// Progress-issue publication was removed (#340); this marker survives only to -// recognize leftover machine-managed issue bodies in triage-collect. -const PROGRESS_MARKER_PREFIX = ""; - function slugify(text) { return text .replace(/[^a-zA-Z0-9]/g, "-") @@ -62,24 +53,6 @@ function slugify(text) { .toLowerCase(); } -function parseMarkerMonth(body) { - if (!body) return null; - const start = body.indexOf(PROGRESS_MARKER_PREFIX); - if (start === -1) return null; - const valueStart = start + PROGRESS_MARKER_PREFIX.length; - const end = body.indexOf(MARKER_SUFFIX, valueStart); - return end === -1 ? null : body.slice(valueStart, end).trim(); -} - -const TRIAGE_CONFIG_DEFAULTS = { - theme_keywords: {}, - activity_days: { - warm: 14, - cold: 60, - }, - stale_days: 60, -}; - function stripQuotes(text) { if ( (text.startsWith('"') && text.endsWith('"')) || @@ -89,13 +62,11 @@ function stripQuotes(text) { } return text; } - function parseInlineArray(raw) { const inner = raw.slice(1, -1).trim(); if (!inner) return []; return inner.split(",").map((part) => stripQuotes(part.trim())); } - function stripYamlSeparationComment(raw) { let quote = null; const firstNonSpace = raw.search(/\S/); @@ -123,7 +94,6 @@ function stripYamlSeparationComment(raw) { } return raw; } - function parseYamlScalar(raw) { const value = stripYamlSeparationComment(raw).trim(); if (!value) return ""; @@ -143,12 +113,10 @@ function parseYamlScalar(raw) { if (value === "false") return false; return value; } - function isBlockScalarValue(raw) { const value = stripYamlSeparationComment(raw).trim(); return /^(?:(?:[&!]\S+)\s+)*(?:[>|](?:[1-9][+-]?|[+-][1-9]?)?)$/.test(value); } - function quotedScalarCloses(text, quote, start = 0) { for (let index = start; index < text.length; index += 1) { if (quote === "'" && text[index] === "'" && text[index + 1] === "'") { @@ -161,7 +129,6 @@ function quotedScalarCloses(text, quote, start = 0) { } return false; } - function parseSimpleYaml(raw) { const root = {}; const stack = [{ indent: -1, value: root }]; @@ -212,38 +179,6 @@ function parseSimpleYaml(raw) { return root; } -function isPlainObject(value) { - return value !== null && typeof value === "object" && !Array.isArray(value); -} - -function mergeConfig(defaults, parsed) { - const merged = { ...defaults }; - for (const [key, value] of Object.entries(parsed || {})) { - if (isPlainObject(value) && isPlainObject(defaults[key])) { - merged[key] = mergeConfig(defaults[key], value); - continue; - } - merged[key] = value; - } - return merged; -} - -function readYamlConfig(configPath, defaults) { - try { - const raw = fs.readFileSync(configPath, "utf-8"); - return mergeConfig(defaults, parseSimpleYaml(raw)); - } catch { - return mergeConfig(defaults, {}); - } -} - -function readTriageConfig(backlogDir) { - return readYamlConfig( - path.join(backlogDir || DEFAULT_BACKLOG_DIR, "triage-config.yml"), - TRIAGE_CONFIG_DEFAULTS - ); -} - /** * Resolve a sprint's scope key from its frontmatter (multi-track partitioning). * Priority: non-empty `component:` wins; else `scope:` path globs; else none. @@ -260,8 +195,7 @@ function sprintScopeKey(frontmatter) { return { kind: "none" }; } -// Reduce a path glob to a comparable directory prefix: -// "src/auth/**" -> "src/auth", "src/auth/*" -> "src/auth", "src/auth/" -> "src/auth". +// Path glob -> directory prefix: "src/auth/**" / "src/auth/*" / "src/auth/" -> "src/auth". function normalizeScopePrefix(glob) { return String(glob).replace(/\/+\**$/, "").replace(/\/+$/, ""); } @@ -293,16 +227,13 @@ function scopesOverlap(frontmatterA, frontmatterB) { module.exports = { slugify, - parseMarkerMonth, parseSimpleYaml, sprintScopeKey, scopesOverlap, - readTriageConfig, ISSUE_REF_RE, parseIssueRef, parsePlanCheckbox, containsIssueRef, - TRIAGE_CONFIG_DEFAULTS, DEFAULT_BACKLOG_DIR, GH_EXEC_DEFAULTS, }; diff --git a/tests/dev-backlog/backlog-doctor.test.js b/tests/dev-backlog/backlog-doctor.test.js index 1bbce7d..62d7ffa 100644 --- a/tests/dev-backlog/backlog-doctor.test.js +++ b/tests/dev-backlog/backlog-doctor.test.js @@ -112,19 +112,17 @@ describe("parseArgs", () => { it("uses the documented defaults", () => { const parsed = parseArgs([]); assert.equal(parsed.backlogDir, ".dev-backlog"); - assert.equal(parsed.staleDays, 7); assert.equal(parsed.json, false); }); - it("accepts --json, --stale-days, and a backlog directory", () => { - const parsed = parseArgs(["--json", "--stale-days", "3", "custom-backlog"]); + it("accepts --json and a backlog directory", () => { + const parsed = parseArgs(["--json", "custom-backlog"]); assert.equal(parsed.json, true); - assert.equal(parsed.staleDays, 3); assert.equal(parsed.backlogDir, "custom-backlog"); }); - it("rejects invalid stale-day values", () => { - assert.match(parseArgs(["--stale-days", "soon"]).error, /Invalid --stale-days/); + it("rejects --stale-days as an unknown argument", () => { + assert.match(parseArgs(["--stale-days"]).error, /Unknown argument: --stale-days/); }); }); @@ -154,8 +152,6 @@ describe("runDoctor", () => { "active_sprint", "sprint_shape", "in_flight_trace", - "in_flight_staleness", - "context_bloat", ]); assert.ok(formatHumanSummary(report).includes("[PASS] active_sprint")); // G4: a single active track never grows track tags (text and JSON alike). @@ -218,7 +214,7 @@ describe("runDoctor", () => { assert.match(check(report, "active_sprint").detail.summary, /2 active tracks, scopes disjoint/); assert.equal(exitCodeFor(report), 0); - for (const name of ["sprint_shape", "in_flight_trace", "in_flight_staleness"]) { + for (const name of ["sprint_shape", "in_flight_trace"]) { const fanned = report.checks.filter((item) => item.name === name); assert.equal(fanned.length, 2, `${name} should run once per track`); assert.deepEqual(fanned.map((item) => item.track), ["2026-07-auth", "2026-07-billing"]); @@ -412,42 +408,4 @@ describe("runDoctor", () => { assert.equal(exitCodeFor(report), 0); assert.equal(report.exit_hint, "warn"); }); - - it("warns on stale in-flight work beyond --stale-days without failing", () => { - seedCleanRepo( - repoRoot, - sprint({ - started: "2026-07-01", - plan: "- [~] #1 Needs follow-up [branch:doctor-test]", - }), - ); - - const report = runDoctor({ - repoRoot, - staleDays: 3, - today: new Date("2026-07-05T00:00:00Z"), - }); - - assert.equal(check(report, "in_flight_staleness").status, "warn"); - assert.equal(check(report, "in_flight_staleness").detail.stale_days, 3); - assert.deepEqual( - check(report, "in_flight_staleness").detail.items.map((item) => item.issue_number), - [1], - ); - assert.equal(exitCodeFor(report), 0); - }); - - it("warns when _context.md exceeds the documented line threshold without failing", () => { - seedCleanRepo(repoRoot); - write( - path.join(repoRoot, ".dev-backlog", "sprints", "_context.md"), - Array.from({ length: 201 }, (_, i) => `line ${i + 1}`).join("\n"), - ); - - const report = runDoctor({ repoRoot }); - - assert.equal(check(report, "context_bloat").status, "warn"); - assert.equal(check(report, "context_bloat").detail.threshold_lines, 200); - assert.equal(exitCodeFor(report), 0); - }); }); diff --git a/tests/dev-backlog/lib.test.js b/tests/dev-backlog/lib.test.js index bab5e74..ea4f74f 100644 --- a/tests/dev-backlog/lib.test.js +++ b/tests/dev-backlog/lib.test.js @@ -1,17 +1,14 @@ -const { describe, it, beforeEach, afterEach } = require("node:test"); +const { describe, it } = require("node:test"); const assert = require("node:assert/strict"); -const fs = require("fs"); const path = require("path"); const SKILL_SCRIPTS = path.resolve(__dirname, "../../skills/dev-backlog/scripts"); const { slugify, scopesOverlap, - readTriageConfig, parseSimpleYaml, parseIssueRef, parsePlanCheckbox, containsIssueRef, - TRIAGE_CONFIG_DEFAULTS, } = require(path.join(SKILL_SCRIPTS, "lib.js")); // --- slugify --- @@ -188,50 +185,6 @@ describe("parseSimpleYaml", () => { }); }); -describe("readTriageConfig", () => { - const tmpDir = path.join(__dirname, "__tmp_triage_config_test__"); - - beforeEach(() => { - fs.mkdirSync(tmpDir, { recursive: true }); - }); - - afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); - }); - - it("returns nested defaults when the config file is missing", () => { - const config = readTriageConfig(path.join(tmpDir, "missing")); - assert.deepEqual(config, TRIAGE_CONFIG_DEFAULTS); - }); - - it("reads nested theme keywords and activity thresholds", () => { - fs.writeFileSync( - path.join(tmpDir, "triage-config.yml"), - [ - "theme_keywords:", - " auth: [auth, oauth, token]", - " docs: [docs, readme]", - "activity_days:", - " warm: 10", - " cold: 45", - "stale_days: 75", - "closed_issue_days: 30", - "", - ].join("\n") - ); - - const config = readTriageConfig(tmpDir); - - assert.deepEqual(config.theme_keywords, { - auth: ["auth", "oauth", "token"], - docs: ["docs", "readme"], - }); - assert.deepEqual(config.activity_days, { warm: 10, cold: 45 }); - assert.equal(config.stale_days, 75); - assert.equal(config.closed_issue_days, 30); - }); -}); - describe("scopesOverlap", () => { it("component: overlaps only on exact equality", () => { assert.equal(scopesOverlap({ component: "auth" }, { component: "auth" }), true);