@techierathore/techieflow 1.0.2 → 1.0.4
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 -13
- package/.claude/commands/TechieFlow/agents/architect.md +1 -16
- package/.claude/commands/TechieFlow/agents/flow-master.md +42 -215
- package/.claude/commands/TechieFlow/agents/verifier.md +24 -64
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +26 -0
- package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +21 -143
- package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
- 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 +31 -505
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
- package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
- package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
- package/.claude/commands/TechieFlow/tasks/facilitate-brainstorming-session.md +4 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
- package/.claude/commands/TechieFlow/tasks/generate-html.md +5 -1
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +18 -16
- package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +3 -1
- package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
- package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
- package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
- package/.opencode/plugin/techieflow.js +40 -13
- package/.tfcore/agents/analyst.md +5 -13
- package/.tfcore/agents/architect.md +1 -16
- package/.tfcore/agents/flow-master.md +42 -215
- package/.tfcore/agents/verifier.md +24 -64
- package/.tfcore/core-config.yaml +10 -0
- package/.tfcore/hooks/block-git.sh +4 -4
- package/.tfcore/hooks/guard-artifacts.sh +2 -2
- package/.tfcore/hooks/guard-build.sh +76 -0
- package/.tfcore/hooks/guard-db.sh +90 -0
- package/.tfcore/hooks/guard-metrics.sh +68 -0
- package/.tfcore/hooks/guard-status-html.sh +116 -71
- package/.tfcore/hooks/guard-status.sh +43 -16
- package/.tfcore/hooks/guard-verify-deps.sh +124 -0
- package/.tfcore/hooks/guard-verify.sh +70 -48
- package/.tfcore/hooks/sweep-artifacts.sh +2 -2
- package/.tfcore/install-manifest.yaml +0 -18
- package/.tfcore/routing.yaml +86 -14
- package/.tfcore/standards/coding-standards-core.md +61 -0
- package/.tfcore/standards/coding-standards-dotnet.md +93 -0
- package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
- package/.tfcore/tasks/_owner-language.md +26 -0
- package/.tfcore/tasks/_smoke-test-policy.md +20 -84
- package/.tfcore/tasks/_status-update-gate.md +10 -74
- package/.tfcore/tasks/_yolo-mode.md +20 -58
- package/.tfcore/tasks/amend-docs.md +21 -143
- package/.tfcore/tasks/build-phase.md +14 -224
- 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 +31 -505
- package/.tfcore/tasks/day1-greenfield.md +28 -213
- package/.tfcore/tasks/deploy-checklist.md +19 -0
- package/.tfcore/tasks/devguide.md +10 -219
- package/.tfcore/tasks/facilitate-brainstorming-session.md +4 -0
- package/.tfcore/tasks/fix-issues.md +10 -129
- package/.tfcore/tasks/generate-html.md +5 -1
- package/.tfcore/tasks/handoff-phase.md +18 -16
- package/.tfcore/tasks/log-miss.md +19 -158
- package/.tfcore/tasks/metrics-report.md +3 -1
- package/.tfcore/tasks/mockups.md +12 -117
- package/.tfcore/tasks/refresh-status.md +9 -130
- package/.tfcore/tasks/split-brd.md +10 -126
- package/.tfcore/tasks/triage-and-fix.md +14 -0
- package/.tfcore/tasks/triage-issues.md +11 -152
- package/.tfcore/tasks/verify-phase.md +36 -572
- package/.tfcore/telemetry/SCHEMA.md +100 -8
- package/.tfcore/telemetry/tf-metrics.sh +564 -32
- package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
- package/.tfcore/templates/stack-questions.md +77 -0
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
- package/.tfcore/templates/v4custom/app-brief-tmpl.md +65 -0
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
- package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
- package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
- package/.tfcore/templates/v4custom/app-decision-request-tmpl.md +85 -0
- package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
- package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
- package/.tfcore/templates/v4custom/app-library-feedback-tmpl.md +86 -31
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
- package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
- package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
- package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
- package/.tfcore/templates/v4custom/html-render-shell.md +1 -1
- package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
- package/.tfcore/user-guide.md +0 -23
- 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-codex-bind.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-telemetry.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-brd-status.py +176 -0
- package/.tfcore/utils/tf-brd-status.sh +19 -0
- package/.tfcore/utils/tf-build-list.py +211 -0
- package/.tfcore/utils/tf-build-list.sh +21 -0
- package/.tfcore/utils/tf-build.sh +143 -0
- package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
- package/.tfcore/utils/tf-checklist-edit.py +133 -0
- package/.tfcore/utils/tf-day1-files.py +208 -0
- package/.tfcore/utils/tf-day1-files.sh +20 -0
- package/.tfcore/utils/tf-devguide-list.py +249 -0
- package/.tfcore/utils/tf-devguide-list.sh +18 -0
- package/.tfcore/utils/tf-doc-check.py +1369 -0
- package/.tfcore/utils/tf-doc-check.sh +29 -0
- package/.tfcore/utils/tf-emit.sh +545 -18
- package/.tfcore/utils/tf-fix-close.sh +115 -0
- package/.tfcore/utils/tf-gitignore-audit.sh +50 -3
- package/.tfcore/utils/tf-goal.sh +284 -66
- package/.tfcore/utils/tf-harness.sh +4 -9
- package/.tfcore/utils/tf-log-miss.py +197 -0
- package/.tfcore/utils/tf-log-miss.sh +14 -0
- package/.tfcore/utils/tf-misses-md.py +208 -0
- package/.tfcore/utils/tf-misses-md.sh +14 -0
- package/.tfcore/utils/tf-mockups-locate.py +66 -0
- package/.tfcore/utils/tf-mockups-locate.sh +18 -0
- package/.tfcore/utils/tf-model-pick.sh +475 -0
- package/.tfcore/utils/tf-opencode-audit.py +23 -7
- package/.tfcore/utils/tf-perf-grade.py +97 -0
- package/.tfcore/utils/tf-perf-grade.sh +12 -0
- package/.tfcore/utils/tf-phase.sh +92 -0
- package/.tfcore/utils/tf-render-html.py +20 -0
- package/.tfcore/utils/tf-routing-bind.sh +97 -6
- package/.tfcore/utils/tf-routing.sh +13 -7
- package/.tfcore/utils/tf-selfcheck.sh +323 -0
- package/.tfcore/utils/tf-split-brd.py +309 -0
- package/.tfcore/utils/tf-split-brd.sh +22 -0
- package/.tfcore/utils/tf-status-evidence.sh +68 -0
- package/.tfcore/utils/tf-status-facts.py +358 -0
- package/.tfcore/utils/tf-status-facts.sh +20 -0
- package/.tfcore/utils/tf-triage.py +243 -0
- package/.tfcore/utils/tf-triage.sh +16 -0
- package/.tfcore/utils/tf-verify-boot.sh +250 -0
- package/.tfcore/utils/tf-verify-emit.sh +110 -0
- package/.tfcore/utils/tf-verify-env.sh +88 -0
- package/.tfcore/utils/tf-verify-list.py +270 -0
- package/.tfcore/utils/tf-verify-list.sh +13 -0
- package/.tfcore/utils/tf-verify-screens.mjs +355 -0
- package/.tfcore/utils/tf-verify-screens.sh +31 -0
- package/.tfcore/utils/tf-verify-tests.sh +166 -0
- package/.tfcore/utils/tf-verify-verdict.py +292 -0
- package/.tfcore/utils/tf-verify-verdict.sh +15 -0
- package/.tfcore/utils/tf-yolo.sh +59 -0
- package/README.md +120 -1145
- package/docs/TechieFlow-Installation.md +1 -5
- package/opencode.jsonc +37 -33
- package/package.json +1 -5
- package/scripts/install.mjs +56 -46
- package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
- package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
- package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
- package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
- package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
- package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
- package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
- package/.codex/config.toml +0 -46
- package/.codex/hooks.json +0 -63
- package/.codex/rules/techieflow.rules +0 -8
- package/.tfcore/hooks/codex-adapter.py +0 -248
- package/.tfcore/tasks/advanced-elicitation.md +0 -117
- package/.tfcore/tasks/author-brd.md +0 -79
- package/.tfcore/tasks/document-project.md +0 -343
- package/.tfcore/tasks/execute-checklist.md +0 -86
- package/.tfcore/tasks/index-docs.md +0 -173
- package/.tfcore/tasks/kb-mode-interaction.md +0 -75
- package/.tfcore/tasks/shard-doc.md +0 -185
- 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
- package/.tfcore/utils/tf-codex-bind.py +0 -196
- package/.tfcore/utils/tf-codex-telemetry.py +0 -60
- package/WORKFLOW.html +0 -1986
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""tf-log-miss.py — one miss record from one sentence (see tf-log-miss.sh).
|
|
3
|
+
|
|
4
|
+
bash .tfcore/utils/tf-log-miss.sh <App> --what "<the owner's sentence>" --sort spec|unsaid|weak-check|ignored
|
|
5
|
+
[--req REQ-UI-014 | --new "<title>" --acceptance "<When … on <screen>, then …>" [--prefix FN] [--section "<screen>"]]
|
|
6
|
+
[--class <miss_class>] [--why <why_missed>] [--artifact <artifact>] [--severity blocker|major|minor]
|
|
7
|
+
[--found-by owner|production] [--evidence <path>] [--fixed [--fix-run <ISO>]] [--started <ISO>]
|
|
8
|
+
|
|
9
|
+
First the sort (FR-32): --sort answers the four questions, in order, and is required.
|
|
10
|
+
spec the app's spec did not say it -> fix the checklist line; the framework is untouched
|
|
11
|
+
unsaid the framework never said it -> one requirement line plus a check; no prose in a task
|
|
12
|
+
weak-check a check existed and did not catch it -> fix the check (a review becomes a script)
|
|
13
|
+
ignored it was written and not followed -> a hook or script, or delete the rule
|
|
14
|
+
Then, in order: the duplicate check (an open miss of the same class on the row is reported, not
|
|
15
|
+
re-logged; a null why_missed or sort on it is completed with --amend); the origin lookup
|
|
16
|
+
(tf-emit.sh --origin-of); the miss record with the sentence as `what` and the sort; the checklist line
|
|
17
|
+
(the row demoted to Needs re-verify with "⚠ miss <date>: <sentence>", or a new Not Started row with
|
|
18
|
+
BRD-pending when nothing owns it; nothing when the repo has no checklist); with --fixed, a miss-fix
|
|
19
|
+
record instead of a demotion; then the run record (cmd log-miss). The emitter rewrites the readable
|
|
20
|
+
docs/<App>-Misses.md. Prints the report block. Exit 0 · 2 could not run.
|
|
21
|
+
"""
|
|
22
|
+
import datetime
|
|
23
|
+
import importlib.util
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import re
|
|
27
|
+
import subprocess
|
|
28
|
+
import sys
|
|
29
|
+
|
|
30
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
31
|
+
EMIT = os.path.join(HERE, "tf-emit.sh")
|
|
32
|
+
CLASSES = {"missed-requirement", "partial-implementation", "unspecified-gap", "regression", "wrong-behaviour", "standards-violation", "spec-contradiction", "other"}
|
|
33
|
+
DEFAULT_ARTIFACT = {"unspecified-gap": "brd", "spec-contradiction": "brd", "standards-violation": "src"}
|
|
34
|
+
SORTS = ("spec", "unsaid", "weak-check", "ignored")
|
|
35
|
+
SORT_QUESTIONS = """tf-log-miss: --sort is missing. Answer the four questions in order and pass the first that fits:
|
|
36
|
+
1. Did the app's spec say it clearly? no -> --sort spec (fix the checklist line)
|
|
37
|
+
2. Did the framework say it anywhere? no -> --sort unsaid (one requirement line plus a check)
|
|
38
|
+
3. Was there a check, and did it fail to catch it? yes -> --sort weak-check (fix the check)
|
|
39
|
+
4. Was it written and ignored anyway? yes -> --sort ignored (a hook, or delete the rule)"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def die(m):
|
|
43
|
+
print(f"tf-log-miss: {m}", file=sys.stderr)
|
|
44
|
+
sys.exit(2)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def opt(argv, name, default=None):
|
|
48
|
+
return argv[argv.index(name) + 1] if name in argv and argv.index(name) + 1 < len(argv) else default
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def q(*args):
|
|
52
|
+
return subprocess.run(["bash", EMIT, *args], capture_output=True, text=True).stdout.strip()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def emit(stream, rec):
|
|
56
|
+
"""Append one record. Returns (ok, reason) — the reason is what the emitter refused it for,
|
|
57
|
+
so the caller can say the miss was NOT recorded instead of claiming it was (miss 06 of
|
|
58
|
+
2026-09-07: a refused artifact value printed 'Miss logged' and an id that existed nowhere)."""
|
|
59
|
+
p = subprocess.run(["bash", EMIT, stream], input=json.dumps(rec), capture_output=True, text=True)
|
|
60
|
+
msg = (p.stdout + p.stderr).strip()
|
|
61
|
+
ok = p.returncode == 0 and "refus" not in msg.lower() and "error" not in msg.lower()
|
|
62
|
+
reason = ""
|
|
63
|
+
if not ok:
|
|
64
|
+
reason = msg.splitlines()[0][:200] if msg else "no reason printed"
|
|
65
|
+
print(f" {stream}: not written — {reason}")
|
|
66
|
+
return ok, reason
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main(argv):
|
|
70
|
+
if len(argv) < 2 or argv[1] in ("-h", "--help") or "--what" not in argv:
|
|
71
|
+
print(__doc__)
|
|
72
|
+
return 0 if "-h" in argv or "--help" in argv else 2
|
|
73
|
+
app = argv[1]
|
|
74
|
+
what = opt(argv, "--what", "").strip()
|
|
75
|
+
if not what:
|
|
76
|
+
die("--what needs the sentence")
|
|
77
|
+
sort = opt(argv, "--sort")
|
|
78
|
+
if not sort:
|
|
79
|
+
print(SORT_QUESTIONS, file=sys.stderr)
|
|
80
|
+
return 2
|
|
81
|
+
if sort not in SORTS:
|
|
82
|
+
die(f"--sort must be one of {', '.join(SORTS)}")
|
|
83
|
+
req = (opt(argv, "--req") or "").upper() or None
|
|
84
|
+
new_title = opt(argv, "--new")
|
|
85
|
+
cls = opt(argv, "--class") or ("unspecified-gap" if new_title else "wrong-behaviour")
|
|
86
|
+
if cls not in CLASSES:
|
|
87
|
+
die(f"--class must be one of {', '.join(sorted(CLASSES))}")
|
|
88
|
+
why = opt(argv, "--why")
|
|
89
|
+
artifact = opt(argv, "--artifact") or DEFAULT_ARTIFACT.get(cls, "src")
|
|
90
|
+
severity = opt(argv, "--severity", "major")
|
|
91
|
+
found_by = opt(argv, "--found-by", "owner")
|
|
92
|
+
fixed = "--fixed" in argv
|
|
93
|
+
started = opt(argv, "--started") or ""
|
|
94
|
+
if not started:
|
|
95
|
+
mk = subprocess.run(["bash", os.path.join(HERE, "tf-phase.sh"), "show"], capture_output=True, text=True).stdout
|
|
96
|
+
m = re.search(r'"started":"([^"]*)"', mk)
|
|
97
|
+
started = m.group(1) if m else datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
98
|
+
now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
99
|
+
|
|
100
|
+
spec = importlib.util.spec_from_file_location("tf_checklist_edit", os.path.join(HERE, "tf-checklist-edit.py"))
|
|
101
|
+
ce = importlib.util.module_from_spec(spec)
|
|
102
|
+
spec.loader.exec_module(ce)
|
|
103
|
+
phase = 1
|
|
104
|
+
cfg = os.path.join(".tfcore", "core-config.yaml")
|
|
105
|
+
if os.path.isfile(cfg):
|
|
106
|
+
m = re.search(r"(?m)^appPhase:\s*(\d+)", open(cfg, encoding="utf-8").read())
|
|
107
|
+
phase = int(m.group(1)) if m else 1
|
|
108
|
+
cl = os.path.join("docs", f"{app}-Checklist.md" if phase <= 1 else f"{app}-P{phase}-Checklist.md")
|
|
109
|
+
has_cl = os.path.isfile(cl)
|
|
110
|
+
|
|
111
|
+
# 1. duplicate check
|
|
112
|
+
if req:
|
|
113
|
+
opened = q("--open-miss", req)
|
|
114
|
+
if opened and opened.split()[-1] == cls:
|
|
115
|
+
mid = opened.split()[0]
|
|
116
|
+
line = f"already logged as {mid}, still open."
|
|
117
|
+
for fld, val in (("why_missed", why), ("sort", sort)):
|
|
118
|
+
if val:
|
|
119
|
+
r = q("--amend", mid, fld, val)
|
|
120
|
+
line += f" {fld} completed with {val}." if "refus" not in r.lower() else f" ({fld} not amended: {r.split('—')[-1].strip()[:90]})"
|
|
121
|
+
print(f"# Miss not re-logged — {app}\n{line}")
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
# 2. the checklist line
|
|
125
|
+
doc_line = "no checklist in this repo — record only"
|
|
126
|
+
if has_cl:
|
|
127
|
+
if req:
|
|
128
|
+
if fixed:
|
|
129
|
+
doc_line = f"{req} left as it is (--fixed)"
|
|
130
|
+
else:
|
|
131
|
+
prior = ce.demote(cl, req, what, evidence=opt(argv, "--evidence"), source=found_by,
|
|
132
|
+
prefix="⚠ prod bug" if found_by == "production" else "⚠ miss")
|
|
133
|
+
doc_line = f"{req} demoted to Needs re-verify (was {prior['status']})" if prior else f"{req} is not a row of {cl}; record only"
|
|
134
|
+
elif new_title:
|
|
135
|
+
acc = opt(argv, "--acceptance") or die("--new needs --acceptance")
|
|
136
|
+
req = ce.add_row(cl, opt(argv, "--prefix", "FN").upper(), new_title, acc, section=opt(argv, "--section"),
|
|
137
|
+
mockup=opt(argv, "--mockup"), remark=f"logged via *log-miss {ce.TODAY}")
|
|
138
|
+
doc_line = f"new Not Started row {req} (BRD-pending; *amend-docs gives it its BRD item)"
|
|
139
|
+
else:
|
|
140
|
+
doc_line = "no owning row and no --new: record only"
|
|
141
|
+
|
|
142
|
+
# 3. the miss
|
|
143
|
+
mid = q("--next-miss-id")
|
|
144
|
+
rec = {"kind": "miss", "miss_id": mid, "req_id": req, "req_class": req.split("-")[1] if req else None, "miss_class": cls,
|
|
145
|
+
"artifact": artifact, "severity": severity, "why_missed": why, "origin_phase": "day1-greenfield" if cls in ("unspecified-gap", "spec-contradiction") else "build-phase",
|
|
146
|
+
"origin_agent": "analyst" if cls in ("unspecified-gap", "spec-contradiction") else "flow-master",
|
|
147
|
+
"found_by": found_by, "found_phase": "log-miss", "found_gate": None, "found_run_id": started, "failure_class": "other", "what": what[:300],
|
|
148
|
+
"sort": sort}
|
|
149
|
+
attributed = "inferred, no run record found"
|
|
150
|
+
if req and cls not in ("unspecified-gap", "spec-contradiction"):
|
|
151
|
+
o = q("--origin-of", req).split()
|
|
152
|
+
if len(o) >= 3:
|
|
153
|
+
rec["origin_run_id"], rec["origin_phase"], rec["origin_agent"] = o[0], o[1], o[2]
|
|
154
|
+
attributed = f"linked to run {o[0]}"
|
|
155
|
+
ok, refusal = emit("misses", rec)
|
|
156
|
+
|
|
157
|
+
# 4. --fixed
|
|
158
|
+
fix_line = ""
|
|
159
|
+
if fixed and ok:
|
|
160
|
+
fa = q("--next-fix-attempt", mid) or "1"
|
|
161
|
+
fr = {"kind": "miss-fix", "miss_id": mid, "req_id": req, "fix_cmd": "log-miss", "fix_attempt": int(fa) if fa.isdigit() else 1,
|
|
162
|
+
"verdict_after": "Verified", "reopened": False}
|
|
163
|
+
if opt(argv, "--fix-run"):
|
|
164
|
+
fr["fix_run_id"] = opt(argv, "--fix-run")
|
|
165
|
+
fix_ok, _ = emit("misses", fr)
|
|
166
|
+
fix_line = ("closed in the same run" + (f" against run {fr['fix_run_id']}" if "fix_run_id" in fr else " (fix run unknown: costed none)")) if fix_ok else "miss-fix not written"
|
|
167
|
+
|
|
168
|
+
# 5. the run record
|
|
169
|
+
run = {"kind": "run", "app": app, "cmd": "log-miss", "mode": None, "started": started, "ended": now,
|
|
170
|
+
"reqs_touched": [req] if req else [], "reqs_count": 1 if req else 0, "subagents": [], "files_written": 1 if has_cl else 0, "build_result": "not-run"}
|
|
171
|
+
emit("runs", run)
|
|
172
|
+
|
|
173
|
+
if not ok:
|
|
174
|
+
print(f"# Miss NOT recorded — {app}")
|
|
175
|
+
print(f"Refused : {refusal}")
|
|
176
|
+
print(f"Nothing carries the id {mid}: the stream has no record and docs/{app}-Misses.md is unchanged.")
|
|
177
|
+
print(f"Docs : {doc_line}")
|
|
178
|
+
print("Next : fix the value the emitter named and run the same command again.")
|
|
179
|
+
return 2
|
|
180
|
+
|
|
181
|
+
print(f"# Miss logged — {app}")
|
|
182
|
+
print(f"{mid} {cls} / {artifact} / {severity}")
|
|
183
|
+
print(f"Why missed : {why or 'not assessed'}")
|
|
184
|
+
print(f"Whose gap : {sort} — " + {"spec": "the app's spec did not say it; fix the checklist line", "unsaid": "the framework never said it; one requirement line plus a check",
|
|
185
|
+
"weak-check": "the check was too weak; fix the check", "ignored": "said and ignored; a hook, or delete the rule"}[sort])
|
|
186
|
+
print(f"Owning REQ : {req or 'none'}")
|
|
187
|
+
print(f"Attributed : {rec['origin_phase']} ({rec['origin_agent']}) — {attributed}")
|
|
188
|
+
print(f"Found by : {found_by}")
|
|
189
|
+
print(f"Docs : {doc_line}; docs/{app}-Misses.md rewritten")
|
|
190
|
+
if fix_line:
|
|
191
|
+
print(f"Fix : {fix_line}")
|
|
192
|
+
print(f"Next : *fix-issues {app}" + (f" {req}" if req and not fixed else ""))
|
|
193
|
+
return 0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == "__main__":
|
|
197
|
+
sys.exit(main(sys.argv))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-log-miss.sh — one miss record from one sentence (Sitting 4c, 2026-09-06).
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-log-miss.sh <App> --what "<the owner's sentence>" [--req REQ-UI-014 | --new "<title>" --acceptance "<When … on <screen>, then …>"]
|
|
5
|
+
# [--class <miss_class>] [--why <why_missed>] [--artifact <artifact>] [--severity blocker|major|minor]
|
|
6
|
+
# [--found-by owner|production] [--evidence <path>] [--fixed [--fix-run <ISO>]] [--started <ISO>]
|
|
7
|
+
#
|
|
8
|
+
# Duplicate check, origin lookup, the miss record carrying the sentence, the checklist line (a demotion,
|
|
9
|
+
# or a new Not Started row with BRD-pending), a miss-fix instead of a demotion with --fixed, then the run
|
|
10
|
+
# record. Never boots, never fixes, never argues. Details: tf-log-miss.py.
|
|
11
|
+
set -u
|
|
12
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
command -v python3 >/dev/null 2>&1 || { echo "tf-log-miss: python3 is required" >&2; exit 2; }
|
|
14
|
+
exec python3 "$HERE/tf-log-miss.py" "$@"
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""tf-misses-md.py — the readable miss list, docs/<App>-Misses.md (Session 5, 2026-09-07; FR-31).
|
|
3
|
+
|
|
4
|
+
python3 .tfcore/utils/tf-misses-md.py [--root <repo>] [--app <App>] [--quiet]
|
|
5
|
+
|
|
6
|
+
Rebuilds docs/<App>-Misses.md from docs/metrics/misses.jsonl: one row per `miss` record, newest first,
|
|
7
|
+
in three tables (open, fixed, will not fix), each row holding the miss id, the owning row, when and
|
|
8
|
+
by whom it was found, whose gap it was (the `sort` field, folded from any miss-amend) and the `what`
|
|
9
|
+
sentence. Markdown only — there is no HTML sibling: the miss log is read by agents and by the owner
|
|
10
|
+
in markdown, and tf-render-html.py refuses it (owner, 2026-09-08; an HTML copy nobody opened was
|
|
11
|
+
being re-rendered on every miss in every project). tf-emit.sh calls this after every write to the
|
|
12
|
+
misses stream, so the file is never older than the record; run it by hand once on a project whose
|
|
13
|
+
stream predates it. The file is derived: never edit it, edit nothing, log a miss.
|
|
14
|
+
Exit 0 always (telemetry has no veto); the reason for a skipped write is printed unless --quiet.
|
|
15
|
+
"""
|
|
16
|
+
import datetime
|
|
17
|
+
import glob
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
24
|
+
|
|
25
|
+
SORT_WORDS = {
|
|
26
|
+
"spec": "the app's spec",
|
|
27
|
+
"unsaid": "the framework never said it",
|
|
28
|
+
"weak-check": "the check was too weak",
|
|
29
|
+
"ignored": "said and ignored",
|
|
30
|
+
}
|
|
31
|
+
SORT_LEGEND = ("**Whose gap** answers the four questions of the miss protocol: "
|
|
32
|
+
"**the app's spec** did not say it, so the checklist line is fixed; "
|
|
33
|
+
"**the framework never said it**, so one requirement line and a check are added; "
|
|
34
|
+
"**the check was too weak** (a review, or a script that did not fire), so the check is fixed; "
|
|
35
|
+
"**said and ignored**, so the rule becomes a hook or is deleted. "
|
|
36
|
+
"**not sorted** means the record predates the sort or nobody has answered yet; "
|
|
37
|
+
"`bash .tfcore/utils/tf-emit.sh --amend <miss> sort <spec|unsaid|weak-check|ignored>` completes it.")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def find_root(explicit):
|
|
41
|
+
if explicit and os.path.isdir(explicit):
|
|
42
|
+
return os.path.abspath(explicit)
|
|
43
|
+
for k in ("TF_METRICS_ROOT", "CLAUDE_PROJECT_DIR"):
|
|
44
|
+
v = os.environ.get(k)
|
|
45
|
+
if v and os.path.isdir(v):
|
|
46
|
+
return os.path.abspath(v)
|
|
47
|
+
d = os.getcwd()
|
|
48
|
+
while d and d != os.path.dirname(d):
|
|
49
|
+
if os.path.isdir(os.path.join(d, ".tfcore")) or os.path.isdir(os.path.join(d, "docs")):
|
|
50
|
+
return d
|
|
51
|
+
d = os.path.dirname(d)
|
|
52
|
+
return os.getcwd()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def app_name(root, explicit):
|
|
56
|
+
if explicit:
|
|
57
|
+
return explicit
|
|
58
|
+
hits = [h for h in glob.glob(os.path.join(root, "docs", "*-Checklist.md"))
|
|
59
|
+
if not re.search(r"-(Deployment|P\d+)-Checklist\.md$", h)]
|
|
60
|
+
if len(hits) == 1:
|
|
61
|
+
return os.path.basename(hits[0])[: -len("-Checklist.md")]
|
|
62
|
+
return os.path.basename(os.path.abspath(root))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def load(path):
|
|
66
|
+
out = []
|
|
67
|
+
try:
|
|
68
|
+
with open(path, encoding="utf-8") as fh:
|
|
69
|
+
for line in fh:
|
|
70
|
+
line = line.strip()
|
|
71
|
+
if not line:
|
|
72
|
+
continue
|
|
73
|
+
try:
|
|
74
|
+
out.append(json.loads(line))
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
except FileNotFoundError:
|
|
78
|
+
pass
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def cell(text):
|
|
83
|
+
text = (text or "").replace("\r", " ").replace("\n", " ").replace("|", "\\|").strip()
|
|
84
|
+
return text or "—"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def day(ts):
|
|
88
|
+
return (ts or "")[:10] or "—"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def build(root, app):
|
|
92
|
+
recs = load(os.path.join(root, "docs", "metrics", "misses.jsonl"))
|
|
93
|
+
misses = [r for r in recs if r.get("kind") == "miss"]
|
|
94
|
+
by_id = {}
|
|
95
|
+
for i, m in enumerate(misses):
|
|
96
|
+
if not m.get("miss_id"): # a record from before the emitter required an id: still a miss, still a row
|
|
97
|
+
m["miss_id"] = f"(no id, record {i + 1})"
|
|
98
|
+
by_id.setdefault(m["miss_id"], m)
|
|
99
|
+
for a in sorted((r for r in recs if r.get("kind") == "miss-amend"), key=lambda r: r.get("ts") or ""):
|
|
100
|
+
p = by_id.get(a.get("miss_id"))
|
|
101
|
+
if p is not None and a.get("field") and p.get(a["field"]) is None:
|
|
102
|
+
p[a["field"]] = a.get("value")
|
|
103
|
+
latest = {}
|
|
104
|
+
for f in recs:
|
|
105
|
+
if f.get("kind") != "miss-fix" or not f.get("miss_id"):
|
|
106
|
+
continue
|
|
107
|
+
prev = latest.get(f["miss_id"])
|
|
108
|
+
if prev is None or (f.get("ts") or "") >= (prev.get("ts") or ""):
|
|
109
|
+
latest[f["miss_id"]] = f
|
|
110
|
+
|
|
111
|
+
def state(m):
|
|
112
|
+
v = (latest.get(m["miss_id"]) or {}).get("verdict_after")
|
|
113
|
+
return "fixed" if v == "Verified" else "wont-fix" if v == "wont-fix" else "open"
|
|
114
|
+
|
|
115
|
+
groups = {"open": [], "fixed": [], "wont-fix": []}
|
|
116
|
+
for m in sorted(by_id.values(), key=lambda r: (r.get("miss_id") or "", r.get("ts") or ""), reverse=True):
|
|
117
|
+
groups[state(m)].append(m)
|
|
118
|
+
|
|
119
|
+
def what(m):
|
|
120
|
+
w = (m.get("what") or "").strip()
|
|
121
|
+
if w:
|
|
122
|
+
return cell(w)
|
|
123
|
+
bits = [m.get("miss_class") or "?", m.get("artifact") or "?"]
|
|
124
|
+
if m.get("why_missed"):
|
|
125
|
+
bits.append("why: " + m["why_missed"])
|
|
126
|
+
return "no sentence recorded (" + ", ".join(bits) + ")"
|
|
127
|
+
|
|
128
|
+
def found(m):
|
|
129
|
+
who = m.get("found_by") or "?"
|
|
130
|
+
return f"{day(m.get('ts'))} by {who}"
|
|
131
|
+
|
|
132
|
+
def gap(m):
|
|
133
|
+
return SORT_WORDS.get(m.get("sort") or "", "not sorted")
|
|
134
|
+
|
|
135
|
+
def ident(m):
|
|
136
|
+
return m["miss_id"] + (f" ({m['req_id']})" if m.get("req_id") else "")
|
|
137
|
+
|
|
138
|
+
today = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d")
|
|
139
|
+
n = len(by_id)
|
|
140
|
+
lines = [f"# {app} — Misses", "", "| | |", "|---|---|", f"| App | {app} |",
|
|
141
|
+
f"| Count | {n} logged: {len(groups['open'])} open, {len(groups['fixed'])} fixed, {len(groups['wont-fix'])} will not fix |",
|
|
142
|
+
"| Source | `docs/metrics/misses.jsonl`, one row per miss record. Rewritten by `tf-misses-md.sh` on every new record. Never edit it: a wrong row is corrected by a new record. |",
|
|
143
|
+
f"| Updated | {today} |", "", SORT_LEGEND, ""]
|
|
144
|
+
if not n:
|
|
145
|
+
lines += ["No miss has been logged yet.", ""]
|
|
146
|
+
for key, title in (("open", "Open"), ("fixed", "Fixed"), ("wont-fix", "Will not fix")):
|
|
147
|
+
rows = groups[key]
|
|
148
|
+
if not rows:
|
|
149
|
+
continue
|
|
150
|
+
lines += [f"## {title} ({len(rows)})", ""]
|
|
151
|
+
if key == "open":
|
|
152
|
+
lines += ["| Miss | Found | Whose gap | What went wrong |", "|---|---|---|---|"]
|
|
153
|
+
for m in rows:
|
|
154
|
+
lines.append(f"| {ident(m)} | {found(m)} | {gap(m)} | {what(m)} |")
|
|
155
|
+
else:
|
|
156
|
+
lines += ["| Miss | Found | Closed | Whose gap | What went wrong |", "|---|---|---|---|---|"]
|
|
157
|
+
for m in rows:
|
|
158
|
+
f = latest.get(m["miss_id"]) or {}
|
|
159
|
+
closed = day(f.get("ts")) + (f" by {f['fix_cmd']}" if f.get("fix_cmd") else "")
|
|
160
|
+
lines.append(f"| {ident(m)} | {found(m)} | {closed} | {gap(m)} | {what(m)} |")
|
|
161
|
+
lines.append("")
|
|
162
|
+
return "\n".join(lines), n
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def main(argv):
|
|
166
|
+
if "-h" in argv or "--help" in argv:
|
|
167
|
+
print(__doc__)
|
|
168
|
+
return 0
|
|
169
|
+
quiet = "--quiet" in argv
|
|
170
|
+
|
|
171
|
+
def opt(name):
|
|
172
|
+
return argv[argv.index(name) + 1] if name in argv and argv.index(name) + 1 < len(argv) else None
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
root = find_root(opt("--root"))
|
|
176
|
+
app = app_name(root, opt("--app"))
|
|
177
|
+
text, n = build(root, app)
|
|
178
|
+
rel = os.path.join("docs", f"{app}-Misses.md")
|
|
179
|
+
out = os.path.join(root, rel)
|
|
180
|
+
os.makedirs(os.path.dirname(out), exist_ok=True)
|
|
181
|
+
old = None
|
|
182
|
+
try:
|
|
183
|
+
with open(out, encoding="utf-8") as fh:
|
|
184
|
+
old = fh.read()
|
|
185
|
+
except FileNotFoundError:
|
|
186
|
+
pass
|
|
187
|
+
stale = out[:-3] + ".html" # left by a pre-2026-09-08 run, before the miss
|
|
188
|
+
gone = "" # log was ruled an agent document (owner)
|
|
189
|
+
if os.path.isfile(stale):
|
|
190
|
+
os.remove(stale)
|
|
191
|
+
gone = "; removed the stale HTML copy"
|
|
192
|
+
same = lambda s: re.sub(r"\| Updated \| [0-9-]+ \|", "", s) # everything but the date must match
|
|
193
|
+
if old is not None and same(old) == same(text):
|
|
194
|
+
if not quiet:
|
|
195
|
+
print(f"tf-misses-md: {rel} unchanged ({n} misses){gone}")
|
|
196
|
+
return 0
|
|
197
|
+
with open(out, "w", encoding="utf-8", newline="\n") as fh:
|
|
198
|
+
fh.write(text)
|
|
199
|
+
if not quiet:
|
|
200
|
+
print(f"tf-misses-md: wrote {rel} ({n} misses){gone}")
|
|
201
|
+
except Exception as e:
|
|
202
|
+
if not quiet:
|
|
203
|
+
print(f"tf-misses-md: nothing written ({e})")
|
|
204
|
+
return 0
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if __name__ == "__main__":
|
|
208
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-misses-md.sh — rebuild the readable miss list docs/<App>-Misses.md from the misses
|
|
3
|
+
# stream (Session 5, 2026-09-07; FR-31). Markdown only: the miss log is never rendered to
|
|
4
|
+
# HTML. tf-emit.sh runs it after every write to that stream; run it by hand once on a
|
|
5
|
+
# project whose stream is older than this script.
|
|
6
|
+
#
|
|
7
|
+
# bash .tfcore/utils/tf-misses-md.sh [--root <repo>] [--app <App>] [--quiet]
|
|
8
|
+
#
|
|
9
|
+
# Exit 0 always. Details: tf-misses-md.py.
|
|
10
|
+
set -u
|
|
11
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
12
|
+
command -v python3 >/dev/null 2>&1 || { echo "tf-misses-md: python3 is required" >&2; exit 0; }
|
|
13
|
+
python3 "$HERE/tf-misses-md.py" "$@"
|
|
14
|
+
exit 0
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""tf-mockups-locate.py — bring every mockup into docs/mockups/ (see tf-mockups-locate.sh).
|
|
3
|
+
|
|
4
|
+
bash .tfcore/utils/tf-mockups-locate.sh [--dry-run]
|
|
5
|
+
|
|
6
|
+
The framework keeps one mockup folder, docs/mockups/. A brownfield repository may hold
|
|
7
|
+
mockups elsewhere (a root mockups/ folder, a designs/ folder, an html file beside the
|
|
8
|
+
docs). Two folders confused a real project, so this script finds every folder whose
|
|
9
|
+
name says mockup (mockup, mockups, wireframes, designs, screens) outside docs/mockups/,
|
|
10
|
+
docs/OldDocs/, the framework folders, node_modules, bin and obj, and moves each .html,
|
|
11
|
+
.png, .jpg or .svg it holds into docs/mockups/. A name already taken there gets the
|
|
12
|
+
source folder name as a suffix. Prints one line per file moved, then the final list of
|
|
13
|
+
docs/mockups/ files. Exit 0 always; 2 if docs/ cannot be created.
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
import shutil
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
NAMES = {"mockup", "mockups", "wireframe", "wireframes", "designs", "screens", "ui-mockups", "mock-ups"}
|
|
20
|
+
SKIP_DIRS = {".tfcore", ".claude", ".opencode", ".git", "node_modules", "bin", "obj",
|
|
21
|
+
"OldDocs", "tests", "wwwroot"}
|
|
22
|
+
EXT = (".html", ".htm", ".png", ".jpg", ".jpeg", ".svg")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main(argv):
|
|
26
|
+
dry = "--dry-run" in argv
|
|
27
|
+
if "-h" in argv or "--help" in argv:
|
|
28
|
+
print(__doc__)
|
|
29
|
+
return 0
|
|
30
|
+
root = os.getcwd()
|
|
31
|
+
target = os.path.join(root, "docs", "mockups")
|
|
32
|
+
try:
|
|
33
|
+
os.makedirs(target, exist_ok=True)
|
|
34
|
+
except Exception as e:
|
|
35
|
+
print(f"tf-mockups-locate: cannot create docs/mockups ({e})", file=sys.stderr)
|
|
36
|
+
return 2
|
|
37
|
+
moved = 0
|
|
38
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
39
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
40
|
+
rel = os.path.relpath(dirpath, root).replace(os.sep, "/")
|
|
41
|
+
if rel in (".", "docs/mockups") or rel.startswith("docs/mockups/"):
|
|
42
|
+
continue
|
|
43
|
+
if os.path.basename(dirpath).lower() not in NAMES:
|
|
44
|
+
continue
|
|
45
|
+
for fn in sorted(filenames):
|
|
46
|
+
if not fn.lower().endswith(EXT):
|
|
47
|
+
continue
|
|
48
|
+
src = os.path.join(dirpath, fn)
|
|
49
|
+
dest = os.path.join(target, fn)
|
|
50
|
+
if os.path.exists(dest):
|
|
51
|
+
stem, ext = os.path.splitext(fn)
|
|
52
|
+
dest = os.path.join(target, f"{stem}-{os.path.basename(dirpath)}{ext}")
|
|
53
|
+
print(f"tf-mockups-locate: {'would move' if dry else 'moved'} {rel}/{fn} -> docs/mockups/{os.path.basename(dest)}")
|
|
54
|
+
if not dry:
|
|
55
|
+
shutil.move(src, dest)
|
|
56
|
+
moved += 1
|
|
57
|
+
if not dry and not os.listdir(dirpath):
|
|
58
|
+
os.rmdir(dirpath)
|
|
59
|
+
print(f"tf-mockups-locate: removed the empty folder {rel}/")
|
|
60
|
+
files = sorted(f for f in os.listdir(target) if f.lower().endswith(EXT))
|
|
61
|
+
print(f"tf-mockups-locate: {moved} file(s) {'to move' if dry else 'moved'}; docs/mockups/ now holds {len(files)}: " + (", ".join(files) if files else "(none)"))
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
sys.exit(main(sys.argv))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-mockups-locate.sh — bring every mockup in the repository into docs/mockups/.
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-mockups-locate.sh # move, then list docs/mockups/
|
|
5
|
+
# bash .tfcore/utils/tf-mockups-locate.sh --dry-run # only report
|
|
6
|
+
#
|
|
7
|
+
# Finds folders named mockup(s), wireframe(s), designs or screens outside
|
|
8
|
+
# docs/mockups/ and moves their .html/.png/.jpg/.svg files there, so the
|
|
9
|
+
# framework's one mockup folder is the only one. Run at brownfield day-1 and by
|
|
10
|
+
# the mockups task before it links or draws anything.
|
|
11
|
+
# Thin wrapper over tf-mockups-locate.py; Python 3 standard library only.
|
|
12
|
+
set -uo pipefail
|
|
13
|
+
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
15
|
+
echo "tf-mockups-locate: python3 is required (standard library only)." >&2
|
|
16
|
+
exit 2
|
|
17
|
+
fi
|
|
18
|
+
exec python3 "$SELF_DIR/tf-mockups-locate.py" "$@"
|