@techierathore/techieflow 1.0.3 → 1.1.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/.claude/commands/TechieFlow/agents/analyst.md +5 -8
- package/.claude/commands/TechieFlow/agents/architect.md +1 -10
- package/.claude/commands/TechieFlow/agents/flow-master.md +1 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +1 -0
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +27 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +1 -1
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +6 -5
- package/.claude/commands/TechieFlow/tasks/build-phase.md +1 -1
- package/.claude/commands/TechieFlow/tasks/create-deep-research-prompt.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-doc.md +4 -0
- package/.claude/commands/TechieFlow/tasks/create-project-brief.md +22 -0
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +3 -3
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/generate-html.md +7 -3
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +20 -18
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +2 -0
- package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
- package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +1 -1
- package/.opencode/plugin/techieflow.js +181 -9
- package/.tfcore/agents/analyst.md +5 -8
- package/.tfcore/agents/architect.md +1 -10
- package/.tfcore/agents/flow-master.md +1 -0
- package/.tfcore/agents/verifier.md +1 -0
- package/.tfcore/hooks/block-git.sh +4 -2
- package/.tfcore/hooks/guard-build.sh +5 -1
- package/.tfcore/hooks/guard-db.sh +6 -4
- package/.tfcore/hooks/guard-metrics.sh +13 -1
- package/.tfcore/hooks/guard-status-html.sh +86 -2
- package/.tfcore/hooks/guard-verify-deps.sh +124 -0
- package/.tfcore/hooks/guard-verify.sh +8 -3
- package/.tfcore/hooks/sweep-artifacts.sh +46 -0
- package/.tfcore/routing.yaml +61 -0
- package/.tfcore/standards/owner-words.txt +23 -0
- package/.tfcore/tasks/_owner-language.md +27 -0
- package/.tfcore/tasks/_smoke-test-policy.md +1 -1
- package/.tfcore/tasks/_status-update-gate.md +4 -3
- package/.tfcore/tasks/amend-docs.md +6 -5
- package/.tfcore/tasks/build-phase.md +1 -1
- package/.tfcore/tasks/create-deep-research-prompt.md +4 -0
- package/.tfcore/tasks/create-doc.md +4 -0
- package/.tfcore/tasks/create-project-brief.md +22 -0
- package/.tfcore/tasks/day1-brownfield.md +3 -3
- package/.tfcore/tasks/day1-greenfield.md +3 -3
- package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
- package/.tfcore/tasks/fix-issues.md +2 -2
- package/.tfcore/tasks/generate-html.md +7 -3
- package/.tfcore/tasks/handoff-phase.md +20 -18
- package/.tfcore/tasks/metrics-report.md +2 -0
- package/.tfcore/tasks/mockups.md +1 -1
- package/.tfcore/tasks/productguide.md +2 -2
- package/.tfcore/tasks/render-workflow-docs.md +1 -1
- package/.tfcore/tasks/triage-issues.md +2 -2
- package/.tfcore/tasks/verify-phase.md +1 -1
- package/.tfcore/telemetry/SCHEMA.md +61 -1
- package/.tfcore/telemetry/install-metrics.sh +3 -2
- package/.tfcore/telemetry/tf-metrics.sh +435 -21
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +5 -3
- package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
- package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
- package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
- package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
- package/.tfcore/templates/v4custom/html-render-shell.md +4 -2
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-assets-browser.mjs +96 -0
- package/.tfcore/utils/tf-assets.sh +97 -12
- package/.tfcore/utils/tf-brd-status.py +14 -5
- package/.tfcore/utils/tf-build-list.py +99 -11
- package/.tfcore/utils/tf-build-list.sh +5 -3
- package/.tfcore/utils/tf-build.sh +88 -15
- package/.tfcore/utils/tf-ci-repro.py +685 -0
- package/.tfcore/utils/tf-ci-repro.sh +24 -0
- package/.tfcore/utils/tf-devguide-list.py +47 -22
- package/.tfcore/utils/tf-doc-check.py +242 -10
- package/.tfcore/utils/tf-doc-tests.sh +79 -0
- package/.tfcore/utils/tf-emit.sh +413 -19
- package/.tfcore/utils/tf-feedback.sh +13 -0
- package/.tfcore/utils/tf-fix-close.sh +113 -31
- package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
- package/.tfcore/utils/tf-goal.sh +105 -18
- package/.tfcore/utils/tf-harness.sh +9 -4
- package/.tfcore/utils/tf-lock.sh +29 -0
- package/.tfcore/utils/tf-log-miss.py +34 -8
- package/.tfcore/utils/tf-login.mjs +167 -0
- package/.tfcore/utils/tf-misses-md.py +24 -21
- package/.tfcore/utils/tf-misses-md.sh +5 -4
- package/.tfcore/utils/tf-mockup-parity.mjs +427 -63
- package/.tfcore/utils/tf-mockup-parity.sh +14 -2
- package/.tfcore/utils/tf-model-pick.sh +475 -0
- package/.tfcore/utils/tf-owner-text.py +233 -0
- package/.tfcore/utils/tf-owner-text.sh +14 -0
- package/.tfcore/utils/tf-phase.sh +33 -1
- package/.tfcore/utils/tf-portable.sh +230 -0
- package/.tfcore/utils/tf-render-html.py +80 -2
- package/.tfcore/utils/tf-routing-bind.sh +87 -1
- package/.tfcore/utils/tf-routing.sh +7 -0
- package/.tfcore/utils/tf-selfcheck.sh +356 -0
- package/.tfcore/utils/tf-split-brd.py +17 -2
- package/.tfcore/utils/tf-status-evidence.sh +11 -9
- package/.tfcore/utils/tf-status-facts.py +133 -44
- package/.tfcore/utils/tf-triage.py +62 -16
- package/.tfcore/utils/tf-triage.sh +2 -0
- package/.tfcore/utils/tf-verify-boot.sh +263 -43
- package/.tfcore/utils/tf-verify-emit.sh +20 -0
- package/.tfcore/utils/tf-verify-env.sh +33 -4
- package/.tfcore/utils/tf-verify-list.py +75 -8
- package/.tfcore/utils/tf-verify-screens.mjs +254 -43
- package/.tfcore/utils/tf-verify-screens.sh +18 -4
- package/.tfcore/utils/tf-verify-tests.sh +272 -28
- package/.tfcore/utils/tf-verify-verdict.py +59 -11
- package/.tfcore/utils/tf_defect.py +27 -0
- package/.tfcore/utils/tf_feedback.py +209 -0
- package/.tfcore/utils/tf_roadmap.py +35 -0
- package/README.md +19 -5
- package/docs/TechieFlow-Installation.md +5 -3
- package/opencode.jsonc +24 -0
- package/package.json +1 -1
- package/scripts/install.mjs +4 -0
- package/.tfcore/templates/architecture-tmpl.yaml +0 -650
- package/.tfcore/templates/brownfield-architecture-tmpl.yaml +0 -476
- package/.tfcore/templates/competitor-analysis-tmpl.yaml +0 -306
- package/.tfcore/templates/front-end-architecture-tmpl.yaml +0 -218
- package/.tfcore/templates/fullstack-architecture-tmpl.yaml +0 -823
- package/.tfcore/templates/market-research-tmpl.yaml +0 -252
- package/.tfcore/templates/project-brief-tmpl.yaml +0 -221
- package/.tfcore/utils/techieflow-doc-template.md +0 -325
|
@@ -41,6 +41,63 @@ root = sys.argv[1]
|
|
|
41
41
|
ryaml = os.path.join(root, ".tfcore", "routing.yaml")
|
|
42
42
|
manifest_path = os.path.join(root, ".tfcore", ".session", "routing-bind.manifest")
|
|
43
43
|
|
|
44
|
+
# --- migration: a project's routing.yaml is never overwritten -------------
|
|
45
|
+
# `update-framework.sh` leaves a project's own routing.yaml alone on purpose, so a block added to
|
|
46
|
+
# the framework default after the project was scaffolded would never reach it. Both delivery
|
|
47
|
+
# routes run THIS script, so the migration lives here: a missing block is APPENDED, with the
|
|
48
|
+
# Claude aliases filled in (opus/sonnet/haiku exist on every account) and the OpenCode lines left
|
|
49
|
+
# empty, because guessing a model id for a provider this machine may not have would produce a
|
|
50
|
+
# fallback that fails at the moment it is needed. Existing lines are never touched, and a file
|
|
51
|
+
# that already has the block is left exactly as it is.
|
|
52
|
+
MIGRATIONS = [
|
|
53
|
+
("fallbacks:", """
|
|
54
|
+
# Which models this tier drops to while its own is limited, in order, comma-separated.
|
|
55
|
+
# Added by tf-routing-bind.sh. The opencode lines are OpenCode Go models
|
|
56
|
+
# (https://opencode.ai/docs/go); change them if this project uses a different provider —
|
|
57
|
+
# `opencode models` prints what this machine can reach.
|
|
58
|
+
# The "limited until <time>" record is kept in this project, at
|
|
59
|
+
# .tfcore/.session/model-cooldown.json. Uncomment to move it (relative to the project root, or
|
|
60
|
+
# absolute); several projects sharing one file learn about a limit from each other.
|
|
61
|
+
# cooldown_file: .tfcore/.session/model-cooldown.json
|
|
62
|
+
# Full explanation: docs/TechieFlow-Routing-Guide.md §9.
|
|
63
|
+
fallbacks:
|
|
64
|
+
frontier:
|
|
65
|
+
claude: opus, haiku
|
|
66
|
+
opencode: opencode-go/glm-5.3, opencode-go/kimi-k3, openai/gpt-5.6-sol
|
|
67
|
+
standard:
|
|
68
|
+
claude: haiku
|
|
69
|
+
opencode: opencode-go/mimo-v2.5, opencode-go/qwen3.8-max, openai/gpt-5.6-luna
|
|
70
|
+
economy:
|
|
71
|
+
claude: sonnet
|
|
72
|
+
opencode: opencode-go/deepseek-v4-flash, opencode-go/glm-5.3-flash, openai/gpt-5.6-luna
|
|
73
|
+
"""),
|
|
74
|
+
("billing:", """
|
|
75
|
+
# How each harness is paid for, so a cost figure knows what it means (SCHEMA.md §2.5b).
|
|
76
|
+
# subscription = a flat fee, marginal dollars are zero; metered = an API key, real money;
|
|
77
|
+
# local = no bill; auto = work it out per model from OpenCode's own sign-in file.
|
|
78
|
+
# Add a provider by name here to override the guess, e.g. " myserver: local".
|
|
79
|
+
billing:
|
|
80
|
+
claude: subscription
|
|
81
|
+
opencode: auto
|
|
82
|
+
"""),
|
|
83
|
+
]
|
|
84
|
+
try:
|
|
85
|
+
_txt = open(ryaml, encoding="utf-8").read()
|
|
86
|
+
_added = []
|
|
87
|
+
for key, block in MIGRATIONS:
|
|
88
|
+
if not re.search(r"^%s\s*$" % re.escape(key), _txt, re.M):
|
|
89
|
+
if not _txt.endswith("\n"):
|
|
90
|
+
_txt += "\n"
|
|
91
|
+
_txt += block
|
|
92
|
+
_added.append(key.rstrip(":"))
|
|
93
|
+
if _added:
|
|
94
|
+
with open(ryaml, "w", encoding="utf-8", newline="\n") as _fh:
|
|
95
|
+
_fh.write(_txt)
|
|
96
|
+
print("tf-routing-bind: added %s to .tfcore/routing.yaml (nothing existing was changed)"
|
|
97
|
+
% " and ".join(_added))
|
|
98
|
+
except FileNotFoundError:
|
|
99
|
+
pass
|
|
100
|
+
|
|
44
101
|
# --- parse routing.yaml (flat format — see its header) --------------------
|
|
45
102
|
cfg = {"enabled": False, "phases": {}, "subagents": {}, "tiers": {}, "effort": {}}
|
|
46
103
|
sect = tier = None
|
|
@@ -67,8 +124,37 @@ try:
|
|
|
67
124
|
except FileNotFoundError:
|
|
68
125
|
pass # no routing.yaml -> treat as disabled
|
|
69
126
|
|
|
127
|
+
# A tier resolves to its own model UNLESS that model is on cooldown — a usage limit recorded by
|
|
128
|
+
# tf-goal.sh in the per-machine file tf-model-pick.sh owns. Binding the fallback here is what stops
|
|
129
|
+
# the sub-agents failing behind a main agent that already moved (Routing-Guide §8, 2026-09-06).
|
|
130
|
+
_PICK = os.path.join(root, ".tfcore", "utils", "tf-model-pick.sh")
|
|
131
|
+
_picked = {}
|
|
132
|
+
|
|
133
|
+
|
|
70
134
|
def model_for(t, harness):
|
|
71
|
-
|
|
135
|
+
declared = cfg["tiers"].get(t, {}).get(harness)
|
|
136
|
+
if not declared or t == "inherit":
|
|
137
|
+
return declared
|
|
138
|
+
if (t, harness) not in _picked:
|
|
139
|
+
got = declared
|
|
140
|
+
if os.path.isfile(_PICK):
|
|
141
|
+
try:
|
|
142
|
+
import subprocess
|
|
143
|
+
out = subprocess.run(["bash", _PICK, "pick", t, harness],
|
|
144
|
+
capture_output=True, text=True, timeout=20)
|
|
145
|
+
cand = (out.stdout or "").strip().splitlines()
|
|
146
|
+
cand = cand[-1].strip() if cand else ""
|
|
147
|
+
if cand and cand != "inherit":
|
|
148
|
+
got = cand
|
|
149
|
+
elif cand == "inherit":
|
|
150
|
+
got = None # every model in the chain is limited: bind nothing, let it wait
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
if got != declared:
|
|
154
|
+
print("tf-routing-bind: tier %s (%s) is on %s — %s is on cooldown"
|
|
155
|
+
% (t, harness, got or "no model: the whole chain is limited", declared))
|
|
156
|
+
_picked[(t, harness)] = got
|
|
157
|
+
return _picked[(t, harness)]
|
|
72
158
|
|
|
73
159
|
# --- remove previously generated artifacts (manifest-driven) --------------
|
|
74
160
|
removed = 0
|
|
@@ -210,6 +210,13 @@ sub = ["%s=%s" % (k, v) for k, v in sorted(cfg["subagents"].items())]
|
|
|
210
210
|
if sub:
|
|
211
211
|
print("Subagents: " + ", ".join(sub))
|
|
212
212
|
print()
|
|
213
|
+
print("When a model is limited (docs/TechieFlow-Routing-Guide.md §9):")
|
|
214
|
+
print(" what each tier would run right now, and what is parked:")
|
|
215
|
+
print(" bash .tfcore/utils/tf-model-pick.sh status")
|
|
216
|
+
print(" the order a tier falls back in: ... chain <tier> <claude|opencode>")
|
|
217
|
+
print(" unpark by hand: ... clear <harness> [model]")
|
|
218
|
+
print(" The supervisor parks a model by itself on a usage limit; tf-goal.sh --no-fallback stops it.")
|
|
219
|
+
print()
|
|
213
220
|
print("Escalation (ADVISORY — you apply it when you launch; nothing switches a model mid-run):")
|
|
214
221
|
if cfg["escalation"]:
|
|
215
222
|
for ph in sorted(cfg["escalation"]):
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# TechieFlow — the framework's own scripts, run against THIS project's real files.
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-selfcheck.sh [<repo>] [--report]
|
|
5
|
+
#
|
|
6
|
+
# WHY IT EXISTS. Nine framework defects reached TfLens in three weeks. Every one was
|
|
7
|
+
# found mid-build, in the application's repository, and could only be fixed in the
|
|
8
|
+
# framework's — so each cost a switch between repos, a feedback entry, a maintenance
|
|
9
|
+
# sitting and a re-deploy. The defects were not exotic: a checklist with a row that
|
|
10
|
+
# could never clear, a BRD carrying the anchors its own link checker demands, a metrics
|
|
11
|
+
# file holding the framework's own verdicts. They were simply never run against a real
|
|
12
|
+
# project's real files before the project ran them (MISS-TechieFlow-20260909-01).
|
|
13
|
+
#
|
|
14
|
+
# So this runs them here, first, and says which are broken ON THIS PROJECT. A FAIL is a
|
|
15
|
+
# FRAMEWORK defect, never the project's: nothing here grades the application.
|
|
16
|
+
#
|
|
17
|
+
# Run it before a build pass, or any time. It writes nothing, boots nothing, and never
|
|
18
|
+
# runs git. Python 3 standard library only.
|
|
19
|
+
#
|
|
20
|
+
# --report prints a ready-to-paste entry for the project's framework-feedback file, so
|
|
21
|
+
# filing costs one paste instead of a page.
|
|
22
|
+
#
|
|
23
|
+
# EXIT: 0 every framework script works here · 1 at least one is broken · 3 cannot run.
|
|
24
|
+
|
|
25
|
+
set -uo pipefail
|
|
26
|
+
REPO="."; REPORT=0
|
|
27
|
+
while [[ $# -gt 0 ]]; do
|
|
28
|
+
case "$1" in
|
|
29
|
+
--report) REPORT=1; shift ;;
|
|
30
|
+
-h|--help) sed -n '2,23p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
31
|
+
*) REPO="$1"; shift ;;
|
|
32
|
+
esac
|
|
33
|
+
done
|
|
34
|
+
[[ -d "$REPO" ]] || { echo "tf-selfcheck: not a directory: $REPO" >&2; exit 3; }
|
|
35
|
+
command -v python3 >/dev/null 2>&1 || { echo "tf-selfcheck: python3 required" >&2; exit 3; }
|
|
36
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
37
|
+
|
|
38
|
+
TF_REPO="$REPO" TF_UTILS="$HERE" TF_REPORT="$REPORT" python3 - <<'PY'
|
|
39
|
+
import json, os, re, subprocess, sys
|
|
40
|
+
|
|
41
|
+
REPO = os.path.abspath(os.environ["TF_REPO"])
|
|
42
|
+
UTILS = os.environ["TF_UTILS"]
|
|
43
|
+
TELEM = os.path.join(os.path.dirname(UTILS), "telemetry")
|
|
44
|
+
REPORT = os.environ["TF_REPORT"] == "1"
|
|
45
|
+
findings = [] # (script, what is wrong, the one-line fix hint)
|
|
46
|
+
lines = []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def ok(script, msg):
|
|
50
|
+
lines.append("ok %-20s %s" % (script, msg))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def broken(script, msg, hint=""):
|
|
54
|
+
lines.append("FAIL %-20s %s" % (script, msg))
|
|
55
|
+
findings.append((script, msg, hint))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def skip(script, msg):
|
|
59
|
+
lines.append("-- %-20s %s" % (script, msg))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def run(cmd, cwd=REPO, timeout=120):
|
|
63
|
+
try:
|
|
64
|
+
p = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
|
|
65
|
+
return p.returncode, (p.stdout or "") + (p.stderr or "")
|
|
66
|
+
except Exception as e:
|
|
67
|
+
return 99, str(e)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def app_names():
|
|
71
|
+
"""Every application this repo holds a checklist for. The checklist is the framework's
|
|
72
|
+
single source of truth, so its name is the app's name — no config value is trusted,
|
|
73
|
+
because core-config.yaml is rsynced between projects and is the thing most likely to
|
|
74
|
+
be stale (the same reason tf-gitignore-audit detects the stack from the tree)."""
|
|
75
|
+
d = os.path.join(REPO, "docs")
|
|
76
|
+
out = []
|
|
77
|
+
if os.path.isdir(d):
|
|
78
|
+
for f in sorted(os.listdir(d)):
|
|
79
|
+
m = re.match(r"^(.+?)(?:-P(\d+))?-Checklist\.md$", f)
|
|
80
|
+
if m and "Deployment" not in f:
|
|
81
|
+
out.append((m.group(1), int(m.group(2) or 1), os.path.join(d, f)))
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---- 1. the deployed copy is intact ----------------------------------------------------
|
|
86
|
+
# A framework script that will not even parse is the cheapest possible thing to catch and
|
|
87
|
+
# the most expensive to meet mid-build.
|
|
88
|
+
# Parsed in-process and the shells checked in ONE bash, not one subprocess per file:
|
|
89
|
+
# 54 scripts meant 54 process launches and ten seconds, which is enough to make a check
|
|
90
|
+
# that should run at the top of every command feel like a tax and get skipped.
|
|
91
|
+
bad_parse, shells = [], []
|
|
92
|
+
for folder in (UTILS, TELEM):
|
|
93
|
+
if not os.path.isdir(folder):
|
|
94
|
+
continue
|
|
95
|
+
for f in sorted(os.listdir(folder)):
|
|
96
|
+
full = os.path.join(folder, f)
|
|
97
|
+
if f.endswith(".py"):
|
|
98
|
+
try:
|
|
99
|
+
import ast
|
|
100
|
+
ast.parse(open(full, encoding="utf-8", errors="replace").read(), filename=full)
|
|
101
|
+
except SyntaxError:
|
|
102
|
+
bad_parse.append(f)
|
|
103
|
+
elif f.endswith(".sh"):
|
|
104
|
+
shells.append(full)
|
|
105
|
+
if shells:
|
|
106
|
+
script = "for f in " + " ".join('"%s"' % x for x in shells) + \
|
|
107
|
+
'; do bash -n "$f" 2>/dev/null || echo "$f"; done'
|
|
108
|
+
rc, out = run(["bash", "-c", script])
|
|
109
|
+
bad_parse += [os.path.basename(x) for x in out.split() if x.strip()]
|
|
110
|
+
if bad_parse:
|
|
111
|
+
broken("deployed copy", "%d script(s) do not parse: %s" % (len(bad_parse), ", ".join(bad_parse[:6])),
|
|
112
|
+
"re-run update-framework.sh; if it persists the framework shipped a broken script")
|
|
113
|
+
else:
|
|
114
|
+
ok("deployed copy", "every framework script parses")
|
|
115
|
+
|
|
116
|
+
# ---- 2. the working list accounts for every checklist row ------------------------------
|
|
117
|
+
# TF-019: a row can sit at `Needs re-verify` forever, which pinned the mode and dropped
|
|
118
|
+
# every not-started row out of the list AND out of the counts, so a build pass could report
|
|
119
|
+
# a phase finished with rows at 0%. The arithmetic is the check: every row lands in exactly
|
|
120
|
+
# one of open / terminal / Blocked / roadmap.
|
|
121
|
+
apps = app_names()
|
|
122
|
+
if not apps:
|
|
123
|
+
skip("tf-build-list", "no checklist in docs/ yet")
|
|
124
|
+
for app, phase, path in apps:
|
|
125
|
+
rc, out = run(["bash", os.path.join(UTILS, "tf-build-list.sh"), app] +
|
|
126
|
+
([] if phase <= 1 else ["--phase", str(phase)]))
|
|
127
|
+
label = "%s p%d" % (app, phase)
|
|
128
|
+
if rc not in (0, 2):
|
|
129
|
+
broken("tf-build-list", "%s: exited %d" % (label, rc), out.strip().splitlines()[0][:120] if out.strip() else "")
|
|
130
|
+
continue
|
|
131
|
+
m = re.search(r"Mode: (\w+) — (\d+) row\(s\) to build; (\d+) terminal, (\d+) Blocked, (?:(\d+) roadmap, )?(\d+) total", out)
|
|
132
|
+
if not m:
|
|
133
|
+
if "has no REQ rows" in out:
|
|
134
|
+
skip("tf-build-list", "%s: checklist has no requirement rows yet" % label)
|
|
135
|
+
else:
|
|
136
|
+
broken("tf-build-list", "%s: no counts line to check" % label,
|
|
137
|
+
"the output shape changed; the arithmetic can no longer be verified")
|
|
138
|
+
continue
|
|
139
|
+
mode, work, term, blocked, roadmap, total = m.group(1), *(int(x or 0) for x in m.groups()[1:])
|
|
140
|
+
if work + term + blocked + roadmap != total:
|
|
141
|
+
broken("tf-build-list",
|
|
142
|
+
"%s: %d rows are in no category (%d open + %d terminal + %d blocked + %d roadmap ≠ %d total)"
|
|
143
|
+
% (label, total - work - term - blocked - roadmap, work, term, blocked, roadmap, total),
|
|
144
|
+
"a build pass trusting this list would leave those rows at 0%")
|
|
145
|
+
else:
|
|
146
|
+
ok("tf-build-list", "%s: %d rows, all accounted for (%s)" % (label, total, mode))
|
|
147
|
+
|
|
148
|
+
# ---- 3. the BRD this project actually wrote can be read --------------------------------
|
|
149
|
+
# TF-017: the ledger regex required **BRD-N** immediately after the bullet, and a real BRD
|
|
150
|
+
# carries an <a id> anchor there because §9 cross-links need one. It matched 0 of 179 items.
|
|
151
|
+
sys.path.insert(0, UTILS)
|
|
152
|
+
for app, phase, _cl in apps:
|
|
153
|
+
brd = os.path.join(REPO, "docs", "%s-BRD.md" % app if phase <= 1 else "%s-P%d-BRD.md" % (app, phase))
|
|
154
|
+
if not os.path.isfile(brd):
|
|
155
|
+
continue
|
|
156
|
+
text = open(brd, encoding="utf-8", errors="replace").read()
|
|
157
|
+
# A struck-through item (~~**BRD-3**~~ *(removed …)*) is deliberately not a live
|
|
158
|
+
# requirement, and the ledger is right to skip it. Counting it as unread would make
|
|
159
|
+
# this check cry wolf on every mature BRD -- and a check that cries wolf is the one
|
|
160
|
+
# that gets skimmed (TF-012 failed 8 of 10 screens identically and trained exactly that).
|
|
161
|
+
# UNIQUE ids, not occurrences: an id named again in a cross-reference is the same
|
|
162
|
+
# requirement, and counting mentions made every BRD look short by however many times
|
|
163
|
+
# it referred to itself.
|
|
164
|
+
written_ids = {m.group(1) for m in re.finditer(r"\*\*(BRD-\d+)\*\*", text)
|
|
165
|
+
if not re.search(r"~~\s*$", text[max(0, m.start() - 3):m.start()])}
|
|
166
|
+
written = len(written_ids)
|
|
167
|
+
try:
|
|
168
|
+
import importlib.util
|
|
169
|
+
spec = importlib.util.spec_from_file_location("_sb", os.path.join(UTILS, "tf-split-brd.py"))
|
|
170
|
+
mod = importlib.util.module_from_spec(spec)
|
|
171
|
+
spec.loader.exec_module(mod)
|
|
172
|
+
read = len({(i["id"] if isinstance(i, dict) else i[0]) for i in mod.ledger(text)})
|
|
173
|
+
except Exception as e:
|
|
174
|
+
broken("tf-split-brd", "%s: could not read the BRD at all (%s)" % (app, type(e).__name__),
|
|
175
|
+
"*amend-docs --add-missing cannot append checklist rows")
|
|
176
|
+
continue
|
|
177
|
+
if written and read < written:
|
|
178
|
+
broken("tf-split-brd", "%s: reads %d of %d BRD items" % (app, read, written),
|
|
179
|
+
"new requirements must be added to the checklist by hand until this is fixed")
|
|
180
|
+
elif written:
|
|
181
|
+
ok("tf-split-brd", "%s: reads all %d BRD items" % (app, written))
|
|
182
|
+
|
|
183
|
+
# ---- 4. the streams this project's figures are built on --------------------------------
|
|
184
|
+
# TF-015: a run whose `ended` precedes its `started` was stored with a negative or invented
|
|
185
|
+
# duration, and the two readers of the stream then disagreed about the same record.
|
|
186
|
+
runs_p = os.path.join(REPO, "docs", "metrics", "runs.jsonl")
|
|
187
|
+
if not os.path.isfile(runs_p):
|
|
188
|
+
skip("runs.jsonl", "no telemetry in this project yet")
|
|
189
|
+
else:
|
|
190
|
+
neg = impossible = total_r = badline = 0
|
|
191
|
+
for l in open(runs_p, encoding="utf-8", errors="replace"):
|
|
192
|
+
l = l.strip()
|
|
193
|
+
if not l:
|
|
194
|
+
continue
|
|
195
|
+
try:
|
|
196
|
+
r = json.loads(l)
|
|
197
|
+
except Exception:
|
|
198
|
+
badline += 1
|
|
199
|
+
continue
|
|
200
|
+
total_r += 1
|
|
201
|
+
d = r.get("duration_s")
|
|
202
|
+
if isinstance(d, (int, float)) and d < 0:
|
|
203
|
+
neg += 1
|
|
204
|
+
s, e = r.get("started"), r.get("ended")
|
|
205
|
+
if isinstance(s, str) and isinstance(e, str) and e < s:
|
|
206
|
+
impossible += 1
|
|
207
|
+
if badline:
|
|
208
|
+
broken("runs.jsonl", "%d line(s) are not valid JSON" % badline, "those records are invisible to every figure")
|
|
209
|
+
if impossible or neg:
|
|
210
|
+
# NOT a failure. The emitter refuses these now and the reader discards them and
|
|
211
|
+
# prints how many -- so nothing is wrong with the framework here, and nothing can
|
|
212
|
+
# be done about the records either: the stream is append-only and the true start
|
|
213
|
+
# times are gone. Reported once as history, because calling it broken on every
|
|
214
|
+
# command for the life of the project is how a check earns being skimmed.
|
|
215
|
+
skip("runs.jsonl", "%d old record(s) end before they start — discarded from every "
|
|
216
|
+
"duration figure, and the report says so" % impossible)
|
|
217
|
+
elif total_r:
|
|
218
|
+
ok("runs.jsonl", "%d run record(s), none impossible" % total_r)
|
|
219
|
+
|
|
220
|
+
# ---- 5. the framework's own verdicts are not counted as this project's -----------------
|
|
221
|
+
# TF-020: an FR verdict grades a framework requirement, not a screen, and pooling it into
|
|
222
|
+
# an application segment raises that segment's first-pass rate with nothing on the output
|
|
223
|
+
# to show it happened. SCHEMA §3 states the rule; the rollup has to apply it.
|
|
224
|
+
gates_p = os.path.join(REPO, "docs", "metrics", "gates.jsonl")
|
|
225
|
+
if not os.path.isfile(gates_p):
|
|
226
|
+
skip("tf-metrics", "no gate records in this project yet")
|
|
227
|
+
else:
|
|
228
|
+
has_fr = False
|
|
229
|
+
for l in open(gates_p, encoding="utf-8", errors="replace"):
|
|
230
|
+
if '"req_class"' in l and '"FR"' in l:
|
|
231
|
+
has_fr = True
|
|
232
|
+
break
|
|
233
|
+
rc, out = run(["bash", os.path.join(TELEM, "tf-metrics.sh"), "--rollup", REPO, "--json"])
|
|
234
|
+
try:
|
|
235
|
+
data = json.loads(out)
|
|
236
|
+
except Exception:
|
|
237
|
+
broken("tf-metrics", "--rollup did not return readable JSON (exit %d)" % rc,
|
|
238
|
+
"no figure this project publishes can be checked against the reference")
|
|
239
|
+
data = None
|
|
240
|
+
if data is not None:
|
|
241
|
+
segs = data.get("live") or {}
|
|
242
|
+
if has_fr and "framework-requirement" not in segs:
|
|
243
|
+
broken("tf-metrics", "framework verdicts are pooled into %s" % ", ".join(sorted(segs) or ["nothing"]),
|
|
244
|
+
"this project's first-pass rate is overstated")
|
|
245
|
+
else:
|
|
246
|
+
ok("tf-metrics", "rollup reads; %d segment(s), framework verdicts separate" % len(segs))
|
|
247
|
+
|
|
248
|
+
# ---- 6. amendments to the miss log still fold -------------------------------------------
|
|
249
|
+
# TF-006 and TF-018: a field added after a record was written can only be completed by an
|
|
250
|
+
# amendment, and an amendment the reader will not fold is a fact silently lost.
|
|
251
|
+
miss_p = os.path.join(REPO, "docs", "metrics", "misses.jsonl")
|
|
252
|
+
if not os.path.isfile(miss_p):
|
|
253
|
+
skip("misses.jsonl", "no miss records in this project yet")
|
|
254
|
+
else:
|
|
255
|
+
ids, amends, orphan = set(), 0, 0
|
|
256
|
+
for l in open(miss_p, encoding="utf-8", errors="replace"):
|
|
257
|
+
l = l.strip()
|
|
258
|
+
if not l:
|
|
259
|
+
continue
|
|
260
|
+
try:
|
|
261
|
+
r = json.loads(l)
|
|
262
|
+
except Exception:
|
|
263
|
+
continue
|
|
264
|
+
if r.get("kind") == "miss":
|
|
265
|
+
ids.add(r.get("miss_id"))
|
|
266
|
+
elif r.get("kind") == "miss-amend":
|
|
267
|
+
amends += 1
|
|
268
|
+
if r.get("miss_id") not in ids:
|
|
269
|
+
orphan += 1
|
|
270
|
+
if orphan:
|
|
271
|
+
broken("misses.jsonl", "%d amendment(s) name a miss that is not on this stream" % orphan,
|
|
272
|
+
"the information in them is not reaching any figure")
|
|
273
|
+
else:
|
|
274
|
+
ok("misses.jsonl", "%d miss(es), %d amendment(s), none orphaned" % (len(ids), amends))
|
|
275
|
+
|
|
276
|
+
# ---- 7. the checks that read this project's documents run at all -----------------------
|
|
277
|
+
for name, cmd in (("tf-doc-check", ["bash", os.path.join(UTILS, "tf-doc-check.sh")] +
|
|
278
|
+
(["--app", apps[0][0]] if apps else []) + ["--warn"]),
|
|
279
|
+
("tf-gitignore-audit", ["bash", os.path.join(UTILS, "tf-gitignore-audit.sh"), REPO, "--dry-run"])):
|
|
280
|
+
if name == "tf-doc-check" and not apps:
|
|
281
|
+
skip(name, "no application documents yet")
|
|
282
|
+
continue
|
|
283
|
+
rc, out = run(cmd)
|
|
284
|
+
if rc in (99,) or "Traceback" in out:
|
|
285
|
+
broken(name, "crashed on this project's files",
|
|
286
|
+
(out.strip().splitlines() or [""])[-1][:120])
|
|
287
|
+
else:
|
|
288
|
+
first = next((l for l in out.splitlines() if l.strip()), "")
|
|
289
|
+
ok(name, "runs (exit %d)" % rc)
|
|
290
|
+
|
|
291
|
+
# ---- 8. the upstream feedback files read as they are ------------------------------------
|
|
292
|
+
# 2026-09-11: PROJECT-STATUS counted every ### heading as an entry and nothing as fixed, so
|
|
293
|
+
# TfLens read "62 open of 62" and its agent told the owner two problems fixed two days earlier
|
|
294
|
+
# were open (MISS-TechieFlow-20260911-04). The one reader is run here, on the real files, and
|
|
295
|
+
# whatever an upstream team has fixed and this project has not re-checked is named below.
|
|
296
|
+
waiting = []
|
|
297
|
+
sys.path.insert(0, UTILS)
|
|
298
|
+
try:
|
|
299
|
+
import tf_feedback
|
|
300
|
+
fb = tf_feedback.files(REPO)
|
|
301
|
+
if not fb:
|
|
302
|
+
skip("tf_feedback", "no upstream feedback files in this project")
|
|
303
|
+
else:
|
|
304
|
+
n = 0
|
|
305
|
+
for f in fb:
|
|
306
|
+
for e in tf_feedback.entries(f):
|
|
307
|
+
n += 1
|
|
308
|
+
if e["state"] == "fixed":
|
|
309
|
+
waiting.append((e["id"], e["since"], os.path.basename(f)))
|
|
310
|
+
ok("tf_feedback", "%d feedback file(s), %d entries read; %d fixed upstream, not yet re-checked"
|
|
311
|
+
% (len(fb), n, len(waiting)))
|
|
312
|
+
except Exception as e:
|
|
313
|
+
broken("tf_feedback", "crashed on this project's feedback files", str(e)[:120])
|
|
314
|
+
|
|
315
|
+
# ---- output -----------------------------------------------------------------------------
|
|
316
|
+
proj = os.path.basename(REPO.rstrip(os.sep))
|
|
317
|
+
print("# tf-selfcheck — %s — the framework's own scripts against this project's real files" % proj)
|
|
318
|
+
for l in lines:
|
|
319
|
+
print(l)
|
|
320
|
+
print()
|
|
321
|
+
if waiting:
|
|
322
|
+
# Not a defect and not a failure: work this project owes. An upstream fix counts only once it
|
|
323
|
+
# has been re-checked here; until then it must never be reported to the owner as open.
|
|
324
|
+
print("Fixed upstream, not yet re-checked here — never report these as open problems:")
|
|
325
|
+
for rid, since, f in waiting:
|
|
326
|
+
print(" %-12s fixed %-10s %s" % (rid, since or "", f))
|
|
327
|
+
print("Re-check each with its entry's \"Verify from here\" step, then close it:")
|
|
328
|
+
print(' bash .tfcore/utils/tf-feedback.sh <App> --close <ID> "<what you ran and what it showed>"')
|
|
329
|
+
print()
|
|
330
|
+
if not findings:
|
|
331
|
+
print("Every framework script works on this project. Nothing to file.")
|
|
332
|
+
raise SystemExit(0)
|
|
333
|
+
|
|
334
|
+
print("%d framework defect(s) found. These are the FRAMEWORK's, not this project's —" % len(findings))
|
|
335
|
+
print("nothing here graded the application. Do not work around them silently: file them,")
|
|
336
|
+
print("and carry on (a framework defect blocks a row only when it stops the work).")
|
|
337
|
+
if REPORT:
|
|
338
|
+
print()
|
|
339
|
+
print("--- paste into docs/%s-TechieFlow-Feedback.md ---" % proj)
|
|
340
|
+
for i, (script, msg, hint) in enumerate(findings, 1):
|
|
341
|
+
print()
|
|
342
|
+
print("## TF-xxx — %s: %s" % (script, msg))
|
|
343
|
+
print()
|
|
344
|
+
print("- **Severity:** major")
|
|
345
|
+
print("- **Blocks:** no")
|
|
346
|
+
print("- **Repro:** `bash .tfcore/utils/tf-selfcheck.sh` in this repository")
|
|
347
|
+
print("- **Expected:** the script works on this project's files")
|
|
348
|
+
print("- **Actual:** %s" % msg)
|
|
349
|
+
print("- **Encountered in:** tf-selfcheck, %s" % proj)
|
|
350
|
+
if hint:
|
|
351
|
+
print("- **Consequence:** %s" % hint)
|
|
352
|
+
print("- **Not fixable here** — `.tfcore/` is framework-owned and is replaced on the next update.")
|
|
353
|
+
else:
|
|
354
|
+
print("Run again with --report for entries you can paste into the feedback file.")
|
|
355
|
+
raise SystemExit(1)
|
|
356
|
+
PY
|
|
@@ -85,7 +85,12 @@ def ledger(text):
|
|
|
85
85
|
lines = text.splitlines()
|
|
86
86
|
i = 0
|
|
87
87
|
while i < len(lines):
|
|
88
|
-
|
|
88
|
+
# The anchor is not decoration: a BRD's §9 screen inventory links to each item as
|
|
89
|
+
# [BRD-21](#brd-21), markdown generates no id for bold text inside a list item, and
|
|
90
|
+
# tf-doc-check refuses a broken link -- so a real BRD carries `<a id="brd-N"></a>`
|
|
91
|
+
# here and this regex matched NONE of TfLens's 179 items. tests/regression tf_017.
|
|
92
|
+
m = re.match(r"^\s*[-*]\s*(?:<a id=[\"\']?[^\"\'>]*[\"\']?\s*>\s*</a>\s*)?"
|
|
93
|
+
r"\*\*(BRD-\d+)\*\*\s*[—:-]+\s*(.*)$", lines[i])
|
|
89
94
|
if m and m.group(1) not in nfr_ids:
|
|
90
95
|
rid, rest = m.group(1), m.group(2)
|
|
91
96
|
title = re.split(r"\s*(?:\.\s+\*|\s\*Screen|\s·\s\*|\s\*Mockup)", rest, 1)[0].strip().rstrip(".")
|
|
@@ -218,7 +223,17 @@ def run(app, phase, force, add_missing, in_order=False):
|
|
|
218
223
|
existing = read(cl_p) if os.path.isfile(cl_p) else ""
|
|
219
224
|
if existing and not force and not add_missing:
|
|
220
225
|
die(f"{cl_p} already exists; use --add-missing to append rows for new BRD items, or --force to rewrite", 1)
|
|
221
|
-
|
|
226
|
+
# A BRD item already has a row when any row names it: on its `*BRD:*` detail line, or anywhere on
|
|
227
|
+
# its status-table line. The table form used to be read only as a bracket holding exactly one id,
|
|
228
|
+
# so "(BRD-76, Phase 3)" and "(BRD-118, BRD-120, Phase 3)" were invisible and --add-missing
|
|
229
|
+
# appended 44 rows to TfLens phase 3 for 13 new items, 31 of them copies of Verified rows (TF-025).
|
|
230
|
+
mapped = {b for line in re.findall(r"(?m)^.*\*BRD:\*.*$", existing) for b in re.findall(r"BRD-\d+", line)}
|
|
231
|
+
mapped |= {b for row in re.findall(r"(?m)^\|\s*REQ-.*$", existing) for b in re.findall(r"BRD-\d+", row)}
|
|
232
|
+
# ...and on the requirement's own line in its detail entry, with the lines indented under it. A
|
|
233
|
+
# checklist migrated from an older plan names its items there, as "(BRD-1, BRD-2)" or
|
|
234
|
+
# "*(BRD-84, BRD-85)*", with no *BRD:* line: AppManager's run appended a row for all 88 items (TF-001).
|
|
235
|
+
for block in re.findall(r"(?m)^\s*[-*]\s*(?:<a id=[^>]*>\s*</a>\s*)?\*\*REQ-[A-Z]+-\d{3}\*\*.*(?:\n[ \t]+\S.*)*", existing):
|
|
236
|
+
mapped |= set(re.findall(r"BRD-\d+", block))
|
|
222
237
|
counters = other_phase_counters(app, cl_p, below=phase if in_order else None)
|
|
223
238
|
if add_missing:
|
|
224
239
|
for m in re.finditer(r"REQ-(UI|FN|RAG|NFR)-(\d{3})", existing):
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# 4. the build verdict from tf-build.sh (skipped with --no-build)
|
|
13
13
|
# Then the agent decides the interruption signature and runs tf-status-facts.sh for the gate.
|
|
14
14
|
set -uo pipefail
|
|
15
|
+
source "$(dirname "${BASH_SOURCE[0]}")/tf-portable.sh" # GNU-only commands, for a stock Mac
|
|
15
16
|
APP="${1:-}"; [[ -z "$APP" ]] && { sed -n '2,12p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 2; }
|
|
16
17
|
shift
|
|
17
18
|
BUILD=1; PHASE=""
|
|
@@ -22,7 +23,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
22
23
|
*) shift ;;
|
|
23
24
|
esac
|
|
24
25
|
done
|
|
25
|
-
[[ -z "$PHASE" ]] && PHASE="$(grep -oE '^appPhase
|
|
26
|
+
[[ -z "$PHASE" ]] && PHASE="$(grep -oE '^appPhase:[[:space:]]*[0-9]+' .tfcore/core-config.yaml 2>/dev/null | grep -oE '[0-9]+' || echo 1)"
|
|
26
27
|
[[ "$PHASE" -le 1 ]] && CL="docs/${APP}-Checklist.md" || CL="docs/${APP}-P${PHASE}-Checklist.md"
|
|
27
28
|
ST="PROJECT-STATUS.md"
|
|
28
29
|
|
|
@@ -31,19 +32,20 @@ echo
|
|
|
31
32
|
echo "## 1. What the stale status file claims (a hypothesis, not a fact)"
|
|
32
33
|
if [[ -f "$ST" ]]; then
|
|
33
34
|
grep -E '^(last_updated|current_phase|last_verified_build|last_verified_date):' "$ST" | sed 's/^/- /'
|
|
34
|
-
awk '/^## Next command to run/{p=1;next} /^## /{p=0} p' "$ST" | grep -E '
|
|
35
|
-
echo "- written: $(
|
|
35
|
+
awk '/^## Next command to run/{p=1;next} /^## /{p=0} p' "$ST" | grep -E '^[[:space:]]*/' | sed 's/^[[:space:]]*/- next: /' | head -2
|
|
36
|
+
echo "- written: $(tf_date_from -u "@$(tf_stat_mtime "$ST")" +%Y-%m-%dT%H:%M:%SZ)"
|
|
36
37
|
else
|
|
37
38
|
echo "- no PROJECT-STATUS.md; treat as never written"
|
|
38
39
|
fi
|
|
39
40
|
echo
|
|
40
41
|
echo "## 2. Files changed after the status file was written (src/ and tests/, newest first)"
|
|
41
42
|
if [[ -f "$ST" ]]; then
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
# xargs runs ls only when find listed something (GNU xargs -r; BSD xargs has no -r)
|
|
44
|
+
NEWER=(); _found="$(find src tests -type f -newer "$ST" \
|
|
45
|
+
-not -path '*/bin/*' -not -path '*/obj/*' -not -path '*/node_modules/*' -not -path '*/.artifacts/*' 2>/dev/null)"
|
|
46
|
+
[[ -n "$_found" ]] && tf_read_lines NEWER < <(printf '%s\n' "$_found" | xargs ls -t 2>/dev/null)
|
|
45
47
|
echo "- ${#NEWER[@]} file(s)"
|
|
46
|
-
printf -- '- %s\n' "${NEWER[@]:0:30}"
|
|
48
|
+
printf -- '- %s\n' ${NEWER[@]+"${NEWER[@]:0:30}"}
|
|
47
49
|
[[ ${#NEWER[@]} -gt 30 ]] && echo "- (${#NEWER[@]} in total; the rest with: find src tests -type f -newer PROJECT-STATUS.md)"
|
|
48
50
|
else
|
|
49
51
|
echo "- no status file to compare against"
|
|
@@ -51,9 +53,9 @@ fi
|
|
|
51
53
|
echo
|
|
52
54
|
echo "## 3. Checklist rows that look interrupted ($CL)"
|
|
53
55
|
if [[ -f "$CL" ]]; then
|
|
54
|
-
grep -E '
|
|
56
|
+
grep -E '^[[:space:]]*\|[[:space:]]*`?REQ-' "$CL" | awk -F'|' '{id=$2; st=$4; gsub(/^[ `*]+|[ `*]+$/,"",id); gsub(/^[ ]+|[ ]+$/,"",st); print id" — "st}' \
|
|
55
57
|
| grep -iE ' — (Implemented|In Progress|Needs re-verify|PARTIAL|FAIL)' | sed 's/^/- /' | head -40
|
|
56
|
-
n=$(grep -E '
|
|
58
|
+
n=$(grep -E '^[[:space:]]*\|[[:space:]]*`?REQ-' "$CL" | awk -F'|' '{print $4}' | grep -ciE 'Implemented|In Progress|Needs re-verify|PARTIAL|FAIL' || true)
|
|
57
59
|
[[ "$n" == "0" ]] && echo "- none: every row is terminal, Blocked or not started"
|
|
58
60
|
else
|
|
59
61
|
echo "- $CL does not exist"
|