This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new b029b17dfb Converge TODO-tracker scripts to be repo-agnostic across 
juneau/release-manager/support-console
b029b17dfb is described below

commit b029b17dfb0294d4b20c8948a3f42ae51a9a46f9
Author: James Bognar <[email protected]>
AuthorDate: Tue Aug 18 14:47:15 2026 -0400

    Converge TODO-tracker scripts to be repo-agnostic across 
juneau/release-manager/support-console
    
    Harden scripts/todo-next-id.py and scripts/todo-status-audit.py so all three
    repos' copies are byte-identical below their per-repo constants:
    - Add --root and --allow-missing flags
    - Exit 2 on a missing tracker dir instead of silently printing 1
    - Emit a scan banner to stderr
    - Reconcile structure via DEFAULT_REPO_ROOT
    
    Also mark both scripts executable (644 -> 755) to match their
    #!/usr/bin/env python3 shebangs and ./scripts/... invocation.
---
 scripts/todo-next-id.py      | 102 +++++++++++++++++++++++++++++++++----------
 scripts/todo-status-audit.py |  77 ++++++++++++++++++++++++--------
 2 files changed, 136 insertions(+), 43 deletions(-)

diff --git a/scripts/todo-next-id.py b/scripts/todo-next-id.py
old mode 100644
new mode 100755
index d99f9c803c..5a74ec73f7
--- a/scripts/todo-next-id.py
+++ b/scripts/todo-next-id.py
@@ -12,13 +12,28 @@
 # * specific language governing permissions and limitations under the License.
 # 
***************************************************************************************************************************
 """
-Next-free TODO-id calculator for Apache Juneau's .work/todo/ tracker.
+Next-free TODO-id calculator for this repository's .work/todo/ tracker.
 
-Mirrors the exact scan scope documented in 
agents/skills/juneau-todo-management/SKILL.md's
+Repo-agnostic: the repository root is derived from this file's own location
+(<root>/scripts/todo-next-id.py -> <root>), never hardcoded, so the same body 
works in
+every repository that adopts the convention. Only the REPO_LABEL / SKILL_NAME 
constants
+below and the license header differ between copies.
+
+**Ids are per-repository and start at 1 in each.** This script only ever scans 
the tree it
+lives in. It prints the resolved root to stderr on every run precisely so that 
a run made
+from the wrong working tree is visible rather than silent.
+
+Mirrors the exact scan scope documented in this repo's TODO-management skill, 
in its
 "Adding a new item" and "MAYBE Numbering" sections:
 
   1. Every "[TODO-n]" and bare "TODO-n" token in .work/todo/TODO.md (a 
trailing lowercase
-     letter suffix, e.g. "TODO-174a", is stripped -- only the numeric part 
counts).
+     letter suffix, e.g. "TODO-174a", is stripped -- only the numeric part 
counts). A
+     qualified cross-repo citation such as "juneau:TODO-42" is NOT counted; it 
names an id in
+     another repo's tracker.
+
+     Note that this scan cannot distinguish an illustrative id from a live 
one: writing
+     "for example, TODO-5" anywhere in TODO.md permanently consumes id 5. 
Write "TODO-<n>" in
+     prose.
   2. Every 
"TODO-"/"READY-"/"MAYBE-"/"FINISHED-"/"CANCELLED-<n>[<letter>]-*.md" filename
      directly under .work/todo/ and .work/todo/finished/.
 
@@ -26,25 +41,32 @@ next = 1 + max(all numeric ids found). A child's letter 
suffix (TODO-174a, FINIS
 is ignored for this computation -- only its numeric part counts, per the 
skill's "Numbering"
 rule -- so promoting/splitting a lettered child never consumes a new 
sequential id.
 
-.work/ is gitignored in apache/juneau, so this is pure filesystem/text 
scanning; no git needed.
+.work/ is gitignored, so this is pure filesystem/text scanning; no git needed.
+
+A MISSING .work/todo/ directory is a hard error (exit 2), not an empty scan. 
Silently
+returning "1" from a tree that has no tracker is how ids get reused: it is the 
correct answer
+in a freshly-seeded repo and a catastrophic one in a repo whose tracker you 
failed to find.
+Pass --allow-missing if you genuinely want the empty-tree answer.
 
 Usage:
     ./scripts/todo-next-id.py
     ./scripts/todo-next-id.py --list
-    ./scripts/todo-next-id.py --check 174a
+    ./scripts/todo-next-id.py --check 12
+    ./scripts/todo-next-id.py --root /path/to/other/repo
 
 Options:
+    --root <path>   Repository root to scan (default: the parent of this 
script's directory).
     --list          Print every id currently in use (letter suffixes 
preserved), one per
                     line, sorted numerically then by letter, instead of the 
next free id.
-    --check <id>    Exit 1 with a message if <id> (e.g. "347" or "174a"; a 
leading "TODO-"
+    --check <id>    Exit 1 with a message if <id> (e.g. "12" or "7a"; a 
leading "TODO-"
                     is tolerated) is already in use; exit 0 with a message if 
it's free.
+    --allow-missing Treat an absent .work/todo/ as an empty tracker instead of 
an error.
     --help, -h      Show this help message.
 
-Examples:
-    ./scripts/todo-next-id.py
-    ./scripts/todo-next-id.py --list
-    ./scripts/todo-next-id.py --check 347
-    ./scripts/todo-next-id.py --check TODO-174a
+Exit status:
+    0   Success (or --check found the id free).
+    1   --check found the id already taken, or a malformed --check argument.
+    2   .work/todo/ does not exist under the resolved root (and 
--allow-missing was not given).
 """
 
 from __future__ import annotations
@@ -54,40 +76,50 @@ import re
 import sys
 from pathlib import Path
 
-REPO_ROOT = Path(__file__).resolve().parent.parent
-TODO_DIR = REPO_ROOT / ".work" / "todo"
-FINISHED_DIR = TODO_DIR / "finished"
+# 
---------------------------------------------------------------------------------------
+# The ONLY repo-specific values in this file. Everything below is identical 
across every
+# copy of this script; keep it that way so a fix lands once and is copied 
verbatim.
+# 
---------------------------------------------------------------------------------------
+REPO_LABEL = "Apache Juneau"
+SKILL_NAME = "juneau-todo-management"
+
+DEFAULT_REPO_ROOT = Path(__file__).resolve().parent.parent
 
 # "[TODO-42]" or bare "TODO-42" in TODO.md prose. A trailing run of lowercase 
letters (the
-# child-letter suffix, possibly more than one for grandchildren like "175fa") 
is captured
+# child-letter suffix, possibly more than one for grandchildren like "17fa") 
is captured
 # separately so it can be preserved for --list/--check but ignored for 
numbering.
-TODO_TOKEN_RE = re.compile(r"\bTODO-(\d+)([a-z]*)\b")
+#
+# The leading lookbehind rejects a qualified cross-repo citation -- 
"juneau:TODO-42",
+# "support-console:TODO-42" -- which names an id in ANOTHER repo's tracker and 
must not
+# consume one here. Ids are bare and per-repository, so without this the act 
of writing down
+# that another repo's item blocks you would silently burn a local id.
+TODO_TOKEN_RE = re.compile(r"(?<![\w:])TODO-(\d+)([a-z]*)\b")
 
 # Every lifecycle-state filename directly under .work/todo/ or 
.work/todo/finished/.
 FILENAME_RE = 
re.compile(r"^(?:TODO|READY|MAYBE|FINISHED|CANCELLED)-(\d+)([a-z]*)-.*\.md$")
 
 
-def collect_ids() -> tuple[set, set]:
+def collect_ids(todo_dir: Path) -> tuple[set, set]:
     """
     Scan every source described in the module docstring.
 
     Returns (raw_ids, numeric_ids):
-      - raw_ids:     every distinct id token as it actually appears (e.g. 
"174", "174a",
-                      "312f"), for --list / --check.
+      - raw_ids:     every distinct id token as it actually appears (e.g. 
"17", "17a",
+                      "12f"), for --list / --check.
       - numeric_ids: just the base integer part of each id (letter suffix 
stripped), for
                       computing the next free id.
     """
     raw_ids = set()
     numeric_ids = set()
 
-    todo_md = TODO_DIR / "TODO.md"
+    todo_md = todo_dir / "TODO.md"
     if todo_md.is_file():
         text = todo_md.read_text(encoding="utf-8")
         for m in TODO_TOKEN_RE.finditer(text):
             raw_ids.add(m.group(1) + m.group(2))
             numeric_ids.add(int(m.group(1)))
 
-    for directory in (TODO_DIR, FINISHED_DIR):
+    for directory in (todo_dir, todo_dir / "finished"):
         if not directory.is_dir():
             continue
         for entry in directory.iterdir():
@@ -123,20 +155,42 @@ def normalize_check_id(raw: str) -> str | None:
 
 def main():
     parser = argparse.ArgumentParser(
-        description="Compute the next free .work/todo/ TODO id (see 
agents/skills/juneau-todo-management/SKILL.md).",
+        description=f"Compute the next free .work/todo/ TODO id for 
{REPO_LABEL} (see @{SKILL_NAME}).",
         epilog=__doc__,
         formatter_class=argparse.RawDescriptionHelpFormatter,
     )
+    parser.add_argument("--root", metavar="PATH", help="Repository root to 
scan (default: parent of this script's directory).")
     parser.add_argument("--list", action="store_true", help="Print every id 
currently in use, sorted.")
     parser.add_argument("--check", metavar="ID", help="Exit non-zero if ID is 
already taken.")
+    parser.add_argument("--allow-missing", action="store_true", help="Treat an 
absent .work/todo/ as empty instead of an error.")
     args = parser.parse_args()
 
-    raw_ids, numeric_ids = collect_ids()
+    repo_root = Path(args.root).resolve() if args.root else DEFAULT_REPO_ROOT
+    todo_dir = repo_root / ".work" / "todo"
+
+    # Always announce the tree actually scanned. Ids are per-repository and 
bare, so
+    # "TODO-5" is a different item in each repo; a wrong-tree run must not 
look identical
+    # to a right-tree one. stderr, so `NEXT=$(./scripts/todo-next-id.py)` 
still works.
+    print(f"[{REPO_LABEL}] scanning {todo_dir}", file=sys.stderr)
+
+    if not todo_dir.is_dir():
+        if not args.allow_missing:
+            print(
+                f"ERROR: {todo_dir} does not exist.\n"
+                f"       Ids are per-repository, so an unfound tracker must 
not be reported as an empty\n"
+                f"       one -- that silently hands out id 1 and reuses live 
ids. Check you are in the\n"
+                f"       right working tree, or pass --allow-missing if this 
repo genuinely has no tracker yet.",
+                file=sys.stderr,
+            )
+            return 2
+        print(f"WARNING: {todo_dir} does not exist; treating as empty 
(--allow-missing).", file=sys.stderr)
+
+    raw_ids, numeric_ids = collect_ids(todo_dir)
 
     if args.check is not None:
         normalized = normalize_check_id(args.check)
         if normalized is None:
-            print(f"ERROR: '{args.check}' is not a valid id (expected e.g. 
'347' or '174a').", file=sys.stderr)
+            print(f"ERROR: '{args.check}' is not a valid id (expected e.g. 
'12' or '7a').", file=sys.stderr)
             return 1
         if normalized in raw_ids:
             print(f"TAKEN: {normalized} is already in use.")
diff --git a/scripts/todo-status-audit.py b/scripts/todo-status-audit.py
old mode 100644
new mode 100755
index 176b642337..177260d4a9
--- a/scripts/todo-status-audit.py
+++ b/scripts/todo-status-audit.py
@@ -12,14 +12,19 @@
 # * specific language governing permissions and limitations under the License.
 # 
***************************************************************************************************************************
 """
-Best-effort status/header consistency pre-filter for Apache Juneau's 
.work/todo/ plan files.
+Best-effort status/header consistency pre-filter for this repository's 
.work/todo/ plan files.
+
+Repo-agnostic: the repository root is derived from this file's own location
+(<root>/scripts/todo-status-audit.py -> <root>), never hardcoded, so the same 
body works in
+every repository that adopts the convention. Only the REPO_LABEL / SKILL_NAME 
constants
+below and the license header differ between copies.
 
 Checks every TODO-<id>-*.md / READY-<id>-*.md / MAYBE-<id>-*.md file directly 
under .work/todo/
-(FINISHED-/CANCELLED-*.md archives are explicitly out of scope -- per
-agents/skills/juneau-todo-management/SKILL.md, "status line is not required in 
FINISHED archives")
-against that skill's "Per-file `Current status:` and `Complexity:` header" 
rules, and flags
-candidate inconsistencies. This is a PRE-FILTER, not a validator: it flags 
candidates for a human
-(or agent) to look at, and will not catch everything on older, format-drifted 
files -- tolerant,
+(FINISHED-/CANCELLED-*.md archives are explicitly out of scope -- per this 
repo's
+TODO-management skill, "status line is not required in FINISHED archives") 
against that
+skill's "Per-file `Current status:` and `Complexity:` header" rules, and flags 
candidate
+inconsistencies. This is a PRE-FILTER, not a validator: it flags candidates 
for a human
+(or agent) to look at, and will not catch everything on format-drifted files 
-- tolerant,
 best-effort markdown-header parsing throughout.
 
 Checks performed (each file may accumulate multiple flags):
@@ -47,20 +52,29 @@ Checks performed (each file may accumulate multiple flags):
                                  wording is reserved for MAYBE-*.md files).
   - maybe_prefix_non_parked     A MAYBE-*.md file whose status does NOT start 
with "Parked".
 
+A MISSING scan directory is a hard error (exit 2). An EMPTY-but-present one is 
a clean pass
+(exit 0). The original version conflated the two and returned 0 for both, so 
pointing the
+script at a tree with no tracker produced a reassuring "nothing to flag" -- 
the same silent-zero
+trap as running `rg` over the gitignored .work/ without --no-ignore.
+
 Usage:
     ./scripts/todo-status-audit.py
     ./scripts/todo-status-audit.py --verbose
+    ./scripts/todo-status-audit.py --root /path/to/other/repo
     ./scripts/todo-status-audit.py --dir /path/to/alternate/todo/dir
 
 Options:
-    --dir <path>    Directory to scan (default: .work/todo/ under the repo 
root). Non-recursive --
-                    only *.md files directly in this directory are considered.
+    --root <path>   Repository root; scans <root>/.work/todo/ (default: parent 
of this
+                    script's directory). Ignored if --dir is given.
+    --dir <path>    Exact directory to scan, overriding --root. Non-recursive 
-- only *.md
+                    files directly in this directory are considered.
     --verbose, -v   Also print files that passed every check (default: only 
print flagged files).
     --help, -h      Show this help message.
 
 Exit status:
-    0   No inconsistencies flagged.
+    0   No inconsistencies flagged (including the legitimately-empty-tracker 
case).
     1   At least one file was flagged.
+    2   The scan directory does not exist.
 """
 
 from __future__ import annotations
@@ -70,8 +84,14 @@ import re
 import sys
 from pathlib import Path
 
-REPO_ROOT = Path(__file__).resolve().parent.parent
-DEFAULT_TODO_DIR = REPO_ROOT / ".work" / "todo"
+# 
---------------------------------------------------------------------------------------
+# The ONLY repo-specific values in this file. Everything below is identical 
across every
+# copy of this script; keep it that way so a fix lands once and is copied 
verbatim.
+# 
---------------------------------------------------------------------------------------
+REPO_LABEL = "Apache Juneau"
+SKILL_NAME = "juneau-todo-management"
+
+DEFAULT_REPO_ROOT = Path(__file__).resolve().parent.parent
 
 FILENAME_RE = re.compile(r"^(TODO|READY|MAYBE)-\d+[a-z]*-.*\.md$")
 
@@ -80,7 +100,11 @@ COMPLEXITY_LINE_RE = re.compile(r"^\s*Complexity:\s*(.*)$", 
re.IGNORECASE | re.M
 SECTION_HEADING_RE = re.compile(r"^##\s+(.*)$", re.MULTILINE)
 NUMBERED_ITEM_RE = re.compile(r"^\s*\d+[.)]\s+(.*)$", re.MULTILINE)
 
-RESOLVED_MARKERS = ("resolved", "answered", "decided")
+# Matched on word boundaries. A bare substring test reads "unanswered" / 
"unresolved" -- the
+# most natural wording for an OPEN question -- as containing "answered" / 
"resolved", which
+# silently disables the ready_but_has_open_questions check for exactly the 
case it exists to
+# catch. Still best-effort: an explicit negation like "not resolved" reads as 
resolved.
+RESOLVED_MARKER_RE = re.compile(r"\b(?:resolved|answered|decided)\b")
 
 # Recognized status prefixes (case-insensitive, checked with str.startswith 
after lowercasing) per
 # the skill's "Status wording rules" -- kept separate from TODO/READY vs MAYBE 
since the two file
@@ -95,8 +119,6 @@ MAYBE_STATUS_PREFIX = "parked"
 
 def find_plan_files(todo_dir: Path) -> list:
     """Every TODO-/READY-/MAYBE-<id>[<letter>]-*.md file directly under 
todo_dir, sorted by name."""
-    if not todo_dir.is_dir():
-        return []
     return sorted(p for p in todo_dir.glob("*.md") if 
FILENAME_RE.match(p.name))
 
 
@@ -131,7 +153,7 @@ def open_questions_are_unresolved(section_body: str) -> 
bool:
         start = m.start()
         end = items[i + 1].start() if i + 1 < len(items) else len(section_body)
         block = section_body[start:end].lower()
-        if not any(marker in block for marker in RESOLVED_MARKERS):
+        if not RESOLVED_MARKER_RE.search(block):
             return True
     return False
 
@@ -190,19 +212,36 @@ def audit_file(path: Path) -> list:
 
 def main() -> int:
     parser = argparse.ArgumentParser(
-        description="Best-effort pre-filter for .work/todo/ Current 
status:/Complexity: header inconsistencies.",
+        description=f"Best-effort pre-filter for {REPO_LABEL}'s .work/todo/ 
header inconsistencies (see @{SKILL_NAME}).",
         epilog=__doc__,
         formatter_class=argparse.RawDescriptionHelpFormatter,
     )
-    parser.add_argument("--dir", metavar="PATH", help="Directory to scan 
(default: .work/todo/ under the repo root).")
+    parser.add_argument("--root", metavar="PATH", help="Repository root; scans 
<root>/.work/todo/ (default: parent of this script's directory).")
+    parser.add_argument("--dir", metavar="PATH", help="Exact directory to 
scan, overriding --root.")
     parser.add_argument("--verbose", "-v", action="store_true", help="Also 
print files that passed every check.")
     args = parser.parse_args()
 
-    todo_dir = Path(args.dir) if args.dir else DEFAULT_TODO_DIR
+    if args.dir:
+        todo_dir = Path(args.dir).resolve()
+    else:
+        repo_root = Path(args.root).resolve() if args.root else 
DEFAULT_REPO_ROOT
+        todo_dir = repo_root / ".work" / "todo"
+
+    # Always announce the tree actually scanned, for the same reason 
todo-next-id.py does:
+    # bare per-repository ids make a wrong-tree run indistinguishable from a 
right-tree one.
+    print(f"[{REPO_LABEL}] scanning {todo_dir}", file=sys.stderr)
+
+    # A missing directory and an empty one are NOT the same answer. Missing 
means the caller
+    # is looking at the wrong tree (or the scaffolding was never installed) 
and must be told;
+    # empty means a genuinely clean tracker and is a legitimate pass.
+    if not todo_dir.is_dir():
+        print(f"ERROR: {todo_dir} does not exist -- nothing was scanned. Check 
the working tree, or pass --dir.", file=sys.stderr)
+        return 2
+
     files = find_plan_files(todo_dir)
 
     if not files:
-        print(f"No TODO-/READY-/MAYBE-*.md files found under {todo_dir}.")
+        print(f"No TODO-/READY-/MAYBE-*.md files found under {todo_dir} 
(directory exists and is empty of plan files).")
         return 0
 
     flagged_count = 0

Reply via email to