@techierathore/techieflow 1.0.2 → 1.0.3
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 +0 -5
- package/.claude/commands/TechieFlow/agents/architect.md +0 -6
- package/.claude/commands/TechieFlow/agents/flow-master.md +41 -215
- package/.claude/commands/TechieFlow/agents/verifier.md +23 -64
- package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
- 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 +20 -143
- package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
- 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/fix-issues.md +10 -129
- package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
- package/.claude/commands/TechieFlow/tasks/metrics-report.md +1 -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 +39 -12
- package/.tfcore/agents/analyst.md +0 -5
- package/.tfcore/agents/architect.md +0 -6
- package/.tfcore/agents/flow-master.md +41 -215
- package/.tfcore/agents/verifier.md +23 -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 +56 -0
- package/.tfcore/hooks/guard-status-html.sh +116 -71
- package/.tfcore/hooks/guard-status.sh +43 -16
- 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 +25 -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/_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 +20 -143
- package/.tfcore/tasks/build-phase.md +14 -224
- 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/fix-issues.md +10 -129
- package/.tfcore/tasks/log-miss.md +19 -158
- package/.tfcore/tasks/metrics-report.md +1 -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 +43 -7
- package/.tfcore/telemetry/tf-metrics.sh +152 -17
- package/.tfcore/templates/project-brief-tmpl.yaml +1 -1
- 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-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-deployment-checklist-tmpl.md +86 -0
- package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
- 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/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 +191 -0
- package/.tfcore/utils/tf-build-list.sh +20 -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 +1267 -0
- package/.tfcore/utils/tf-doc-check.sh +29 -0
- package/.tfcore/utils/tf-emit.sh +203 -13
- package/.tfcore/utils/tf-fix-close.sh +83 -0
- package/.tfcore/utils/tf-goal.sh +193 -62
- 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 +211 -0
- package/.tfcore/utils/tf-misses-md.sh +13 -0
- package/.tfcore/utils/tf-mockups-locate.py +66 -0
- package/.tfcore/utils/tf-mockups-locate.sh +18 -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 +74 -0
- package/.tfcore/utils/tf-routing-bind.sh +10 -5
- package/.tfcore/utils/tf-routing.sh +6 -7
- package/.tfcore/utils/tf-split-brd.py +304 -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 +309 -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 +283 -0
- package/.tfcore/utils/tf-verify-screens.sh +22 -0
- package/.tfcore/utils/tf-verify-tests.sh +123 -0
- package/.tfcore/utils/tf-verify-verdict.py +283 -0
- package/.tfcore/utils/tf-verify-verdict.sh +15 -0
- package/.tfcore/utils/tf-yolo.sh +59 -0
- package/README.md +114 -1149
- package/docs/TechieFlow-Installation.md +1 -5
- package/opencode.jsonc +13 -33
- package/package.json +1 -5
- package/scripts/install.mjs +52 -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/utils/tf-codex-bind.py +0 -196
- package/.tfcore/utils/tf-codex-telemetry.py +0 -60
- package/WORKFLOW.html +0 -1986
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""tf-day1-files.py — the mechanical part of day-1 (see tf-day1-files.sh).
|
|
3
|
+
|
|
4
|
+
bash .tfcore/utils/tf-day1-files.sh <App> [--size S|M|L] [--kind app|library] [--prefix obj|none] [--phase N]
|
|
5
|
+
bash .tfcore/utils/tf-day1-files.sh --archive <file> [<file> ...]
|
|
6
|
+
|
|
7
|
+
Default mode, for <App>:
|
|
8
|
+
- .tfcore/core-config.yaml: customTechnicalDocuments (brd, architecture, codingStandards),
|
|
9
|
+
devLoadAlwaysFiles, and appSize / appKind / appPhase when given.
|
|
10
|
+
- --size L also writes docs/<App>-Phases.md from app-phases-tmpl.md when it does not exist
|
|
11
|
+
(the Large layout: docs/TechieFlow-Document-Schemas.md §2, §3.11). The agent fills the table.
|
|
12
|
+
- --phase N sets appPhase, the phase being worked (handoff moves it on).
|
|
13
|
+
- .editorconfig from app-editorconfig-tmpl.editorconfig (overwritten; machine config).
|
|
14
|
+
- AGENTS.md and CLAUDE.md from their templates with {AppName} substituted and the
|
|
15
|
+
field-prefix line resolved (--prefix, default obj). An existing copy is archived first.
|
|
16
|
+
--archive: move each file (and its sibling .html) to docs/OldDocs/, date-suffixed when a
|
|
17
|
+
file of that name is already there. Never asks; never writes a -v2 variant.
|
|
18
|
+
|
|
19
|
+
Prints one line per thing done. Python 3 standard library only. Exit 0 done, 2 could not run.
|
|
20
|
+
"""
|
|
21
|
+
import datetime
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
import shutil
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
TPL = os.path.join(".tfcore", "templates", "v4custom")
|
|
28
|
+
PREFIX_TEXT = {
|
|
29
|
+
"obj": "`obj` prefix on instance fields (e.g. `private readonly ILogger<X> objLogger;`)",
|
|
30
|
+
"none": "bare PascalCase, no prefix",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def die(msg):
|
|
35
|
+
print(f"tf-day1-files: {msg}", file=sys.stderr)
|
|
36
|
+
sys.exit(2)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def read(p):
|
|
40
|
+
with open(p, encoding="utf-8", errors="replace") as f:
|
|
41
|
+
return f.read()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def write(p, s):
|
|
45
|
+
with open(p, "w", encoding="utf-8", newline="\n") as f:
|
|
46
|
+
f.write(s)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def archive(path):
|
|
50
|
+
"""Move path (and its sibling .html) to docs/OldDocs/, date-suffixed on collision."""
|
|
51
|
+
if not os.path.isfile(path):
|
|
52
|
+
return
|
|
53
|
+
old = os.path.join("docs", "OldDocs")
|
|
54
|
+
os.makedirs(old, exist_ok=True)
|
|
55
|
+
for p in (path, os.path.splitext(path)[0] + ".html"):
|
|
56
|
+
if not os.path.isfile(p):
|
|
57
|
+
continue
|
|
58
|
+
dest = os.path.join(old, os.path.basename(p))
|
|
59
|
+
if os.path.exists(dest):
|
|
60
|
+
stem, ext = os.path.splitext(os.path.basename(p))
|
|
61
|
+
dest = os.path.join(old, f"{stem}-{datetime.date.today().strftime('%Y%m%d')}{ext}")
|
|
62
|
+
n = 1
|
|
63
|
+
while os.path.exists(dest):
|
|
64
|
+
n += 1
|
|
65
|
+
dest = os.path.join(old, f"{stem}-{datetime.date.today().strftime('%Y%m%d')}-{n}{ext}")
|
|
66
|
+
shutil.move(p, dest)
|
|
67
|
+
print(f"tf-day1-files: archived {p} -> {dest}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def set_key(text, key, value_lines):
|
|
71
|
+
"""Replace a top-level YAML key (scalar or block) with the given lines; append if absent."""
|
|
72
|
+
lines = text.splitlines()
|
|
73
|
+
out, i, done = [], 0, False
|
|
74
|
+
while i < len(lines):
|
|
75
|
+
if re.match(rf"^{re.escape(key)}:", lines[i]):
|
|
76
|
+
out.extend(value_lines)
|
|
77
|
+
i += 1
|
|
78
|
+
while i < len(lines) and (lines[i].startswith((" ", "\t")) or lines[i].strip() == "" and i + 1 < len(lines) and lines[i + 1].startswith(" ")):
|
|
79
|
+
i += 1
|
|
80
|
+
done = True
|
|
81
|
+
continue
|
|
82
|
+
out.append(lines[i])
|
|
83
|
+
i += 1
|
|
84
|
+
if not done:
|
|
85
|
+
out.extend(value_lines)
|
|
86
|
+
return "\n".join(out) + "\n"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def update_config(app, size, kind, phase):
|
|
90
|
+
p = os.path.join(".tfcore", "core-config.yaml")
|
|
91
|
+
if not os.path.isfile(p):
|
|
92
|
+
die(f"{p} does not exist at its literal path; is the framework installed here?")
|
|
93
|
+
s = read(p)
|
|
94
|
+
s = set_key(s, "customTechnicalDocuments", [
|
|
95
|
+
"customTechnicalDocuments:",
|
|
96
|
+
f" brd: docs/{app}-BRD.md",
|
|
97
|
+
f" architecture: docs/{app}-Architecture.md",
|
|
98
|
+
f" codingStandards: docs/{app}-Coding-Standards.md",
|
|
99
|
+
])
|
|
100
|
+
s = set_key(s, "devLoadAlwaysFiles", [
|
|
101
|
+
"devLoadAlwaysFiles:",
|
|
102
|
+
f" - docs/{app}-Coding-Standards.md",
|
|
103
|
+
f" - docs/{app}-Architecture.md",
|
|
104
|
+
])
|
|
105
|
+
if size:
|
|
106
|
+
s = set_key(s, "appSize", [f"appSize: {size}"])
|
|
107
|
+
if kind:
|
|
108
|
+
s = set_key(s, "appKind", [f"appKind: {kind}"])
|
|
109
|
+
if phase:
|
|
110
|
+
s = set_key(s, "appPhase", [f"appPhase: {phase}"])
|
|
111
|
+
write(p, s)
|
|
112
|
+
print(f"tf-day1-files: .tfcore/core-config.yaml updated (documents for {app}"
|
|
113
|
+
+ (f", appSize {size}" if size else "") + (f", appKind {kind}" if kind else "")
|
|
114
|
+
+ (f", appPhase {phase}" if phase else "") + ")")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def write_phases_doc(app):
|
|
118
|
+
"""docs/<App>-Phases.md from its template, only when absent (Large projects)."""
|
|
119
|
+
dest = os.path.join("docs", f"{app}-Phases.md")
|
|
120
|
+
if os.path.isfile(dest):
|
|
121
|
+
print(f"tf-day1-files: {dest} already exists; left as is")
|
|
122
|
+
return
|
|
123
|
+
src = os.path.join(TPL, "app-phases-tmpl.md")
|
|
124
|
+
if not os.path.isfile(src):
|
|
125
|
+
die(f"template {src} does not exist at its literal path")
|
|
126
|
+
os.makedirs("docs", exist_ok=True)
|
|
127
|
+
body = read(src).replace("{App}", app).replace("{YYYY-MM-DD}", datetime.date.today().isoformat())
|
|
128
|
+
write(dest, body)
|
|
129
|
+
print(f"tf-day1-files: wrote {dest} skeleton; fill the Phases table (one row per phase, each within Medium)")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def write_from_template(app, tmpl, dest, prefix):
|
|
133
|
+
src = os.path.join(TPL, tmpl)
|
|
134
|
+
if not os.path.isfile(src):
|
|
135
|
+
die(f"template {src} does not exist at its literal path")
|
|
136
|
+
body = read(src).replace("{AppName}", app)
|
|
137
|
+
if prefix:
|
|
138
|
+
body = re.sub(r"\{obj prefix on instance fields[^}]*\| bare PascalCase, no prefix\}", PREFIX_TEXT[prefix], body)
|
|
139
|
+
if os.path.isfile(dest) and read(dest) == body:
|
|
140
|
+
print(f"tf-day1-files: {dest} already current")
|
|
141
|
+
return
|
|
142
|
+
if dest != ".editorconfig":
|
|
143
|
+
archive(dest)
|
|
144
|
+
write(dest, body)
|
|
145
|
+
print(f"tf-day1-files: wrote {dest}")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main(argv):
|
|
149
|
+
if len(argv) < 2 or argv[1] in ("-h", "--help"):
|
|
150
|
+
print(__doc__)
|
|
151
|
+
return 0
|
|
152
|
+
if argv[1] == "--archive":
|
|
153
|
+
if len(argv) < 3:
|
|
154
|
+
die("--archive needs at least one file")
|
|
155
|
+
for f in argv[2:]:
|
|
156
|
+
if os.path.isfile(f):
|
|
157
|
+
archive(f)
|
|
158
|
+
else:
|
|
159
|
+
print(f"tf-day1-files: {f} does not exist; nothing to archive")
|
|
160
|
+
return 0
|
|
161
|
+
app = argv[1]
|
|
162
|
+
if not re.fullmatch(r"[A-Z][A-Za-z0-9]*", app):
|
|
163
|
+
die(f"app name {app!r} must be PascalCase with no spaces")
|
|
164
|
+
size = kind = phase = None
|
|
165
|
+
prefix = "obj"
|
|
166
|
+
args = argv[2:]
|
|
167
|
+
i = 0
|
|
168
|
+
while i < len(args):
|
|
169
|
+
a = args[i]
|
|
170
|
+
if a == "--size" and i + 1 < len(args):
|
|
171
|
+
v = args[i + 1].strip().upper()[:1]
|
|
172
|
+
if v not in ("S", "M", "L"):
|
|
173
|
+
die("--size must be S, M or L")
|
|
174
|
+
size = v
|
|
175
|
+
i += 2
|
|
176
|
+
elif a == "--phase" and i + 1 < len(args):
|
|
177
|
+
if not args[i + 1].isdigit() or int(args[i + 1]) < 1:
|
|
178
|
+
die("--phase must be a number from 1 upwards")
|
|
179
|
+
phase = int(args[i + 1])
|
|
180
|
+
i += 2
|
|
181
|
+
elif a == "--kind" and i + 1 < len(args):
|
|
182
|
+
kind = args[i + 1].strip().lower()
|
|
183
|
+
if kind not in ("app", "library"):
|
|
184
|
+
die("--kind must be app or library")
|
|
185
|
+
i += 2
|
|
186
|
+
elif a == "--prefix" and i + 1 < len(args):
|
|
187
|
+
prefix = args[i + 1].strip().lower()
|
|
188
|
+
if prefix not in PREFIX_TEXT:
|
|
189
|
+
die("--prefix must be obj or none")
|
|
190
|
+
i += 2
|
|
191
|
+
else:
|
|
192
|
+
die(f"unknown argument {a!r}")
|
|
193
|
+
update_config(app, size, kind, phase)
|
|
194
|
+
if size == "L":
|
|
195
|
+
write_phases_doc(app)
|
|
196
|
+
write_from_template(app, "app-editorconfig-tmpl.editorconfig", ".editorconfig", None)
|
|
197
|
+
write_from_template(app, "app-agents-md-tmpl.md", "AGENTS.md", prefix)
|
|
198
|
+
write_from_template(app, "app-claude-md-tmpl.md", "CLAUDE.md", prefix)
|
|
199
|
+
return 0
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
try:
|
|
204
|
+
sys.exit(main(sys.argv))
|
|
205
|
+
except SystemExit:
|
|
206
|
+
raise
|
|
207
|
+
except Exception as e:
|
|
208
|
+
die(str(e))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-day1-files.sh — the mechanical part of day-1.
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-day1-files.sh MyApp --size S --kind app # config entries (stage 1)
|
|
5
|
+
# bash .tfcore/utils/tf-day1-files.sh MyApp --prefix obj # .editorconfig, AGENTS.md, CLAUDE.md (stage 2)
|
|
6
|
+
# bash .tfcore/utils/tf-day1-files.sh --archive docs/MyApp-BRD.md ... # move old copies to docs/OldDocs/
|
|
7
|
+
# bash .tfcore/utils/tf-day1-files.sh MyApp --phase 2 # Large project: the phase being worked
|
|
8
|
+
#
|
|
9
|
+
# Writes core-config.yaml keys (customTechnicalDocuments, devLoadAlwaysFiles,
|
|
10
|
+
# appSize, appKind, appPhase), writes docs/MyApp-Phases.md from its template on
|
|
11
|
+
# --size L when absent, and copies the three scaffold files from their templates
|
|
12
|
+
# with {AppName} substituted. Archives an existing AGENTS.md / CLAUDE.md first.
|
|
13
|
+
# Never asks a question. Thin wrapper over tf-day1-files.py; Python 3 stdlib only.
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
17
|
+
echo "tf-day1-files: python3 is required (standard library only)." >&2
|
|
18
|
+
exit 2
|
|
19
|
+
fi
|
|
20
|
+
exec python3 "$SELF_DIR/tf-day1-files.py" "$@"
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""tf-devguide-list.py — the work list for *devguide (see tf-devguide-list.sh).
|
|
3
|
+
|
|
4
|
+
bash .tfcore/utils/tf-devguide-list.sh <App> [--phase N] [--update]
|
|
5
|
+
|
|
6
|
+
Reads appKind and appPhase from .tfcore/core-config.yaml and prints:
|
|
7
|
+
app every routed page found in the code (route, file), matched to the phase's
|
|
8
|
+
UIDesign screens and to the roles in the UsageGuide's Test users table;
|
|
9
|
+
UIDesign screens with no code (a defect), routes with no UIDesign screen
|
|
10
|
+
ui library every public component under src/ (not the samples), with the sample page
|
|
11
|
+
that shows it, or "no sample" (a gap)
|
|
12
|
+
service library every public service: DI registration methods, public interfaces and
|
|
13
|
+
classes under src/, with the sample that calls it, or "no sample"
|
|
14
|
+
--update which existing guide entries have files newer than the guide's "Verified on"
|
|
15
|
+
Prints NOTHING when no code is found. Writes nothing. Python 3 standard library only.
|
|
16
|
+
"""
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
SKIP = ("/bin/", "/obj/", "/node_modules/", "/.artifacts/", "/OldDocs/", "/.git/", "/dist/", "/wwwroot/lib/")
|
|
22
|
+
SAMPLE_DIRS = ("samples", "sample", "demos", "demo", "apps", "examples", "tests")
|
|
23
|
+
ROUTE_PATTERNS = [ # (file pattern, route pattern, page or endpoint)
|
|
24
|
+
(r"\.(razor|cshtml)$", re.compile(r'(?m)^\s*@page\s+"([^"]+)"'), "page"),
|
|
25
|
+
(r"\.cs$", re.compile(r'\[(?:Http(?:Get|Post|Put|Delete)|Route)\(\s*"([^"]+)"'), "endpoint"),
|
|
26
|
+
(r"\.(ts|tsx|js|jsx|vue)$", re.compile(r'''\bpath\s*:\s*['"]([^'"]+)['"]'''), "page"),
|
|
27
|
+
(r"\.(tsx|jsx)$", re.compile(r'''<Route\b[^>]*\bpath\s*=\s*['"]([^'"]+)['"]'''), "page"),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def die(msg):
|
|
32
|
+
print(f"tf-devguide-list: {msg}", file=sys.stderr)
|
|
33
|
+
sys.exit(2)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read(p):
|
|
37
|
+
with open(p, encoding="utf-8-sig", errors="replace") as f: # -sig: a byte-order mark never hides line 1
|
|
38
|
+
return f.read()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cfg(key, default):
|
|
42
|
+
p = os.path.join(".tfcore", "core-config.yaml")
|
|
43
|
+
if os.path.isfile(p):
|
|
44
|
+
m = re.search(rf"(?m)^{key}:\s*(\S+)", read(p))
|
|
45
|
+
if m and m.group(1) not in ("null", "~"):
|
|
46
|
+
return m.group(1).strip("'\"")
|
|
47
|
+
return default
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
PRUNE = {"bin", "obj", "node_modules", ".git", ".artifacts", "OldDocs", "dist", ".tfcore", ".claude", ".opencode", "packages", "TestResults"}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def walk(root, exts, skip_samples=False):
|
|
54
|
+
for d, dirs, files in os.walk(root):
|
|
55
|
+
dirs[:] = [x for x in dirs if x not in PRUNE] # prune in place: never descend into build output
|
|
56
|
+
dd = d.replace("\\", "/") + "/"
|
|
57
|
+
if any(s in dd for s in SKIP):
|
|
58
|
+
continue
|
|
59
|
+
if skip_samples and any(f"/{s}/" in dd or dd.endswith(f"/{s}/") for s in SAMPLE_DIRS):
|
|
60
|
+
continue
|
|
61
|
+
for f in files:
|
|
62
|
+
if f.lower().endswith(exts):
|
|
63
|
+
yield os.path.join(d, f).replace("\\", "/").lstrip("./")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def roles_from_usageguide(app):
|
|
67
|
+
p = os.path.join("docs", f"{app}-UsageGuide.md")
|
|
68
|
+
roles = []
|
|
69
|
+
if os.path.isfile(p):
|
|
70
|
+
m = re.search(r"(?ms)^##\s+(?:\d+[.)]\s*)?Test users[^\n]*\n(.*?)(?=^## |\Z)", read(p))
|
|
71
|
+
if m:
|
|
72
|
+
for line in m.group(1).splitlines():
|
|
73
|
+
c = [x.strip() for x in line.strip().strip("|").split("|")]
|
|
74
|
+
if len(c) >= 4 and c[0].strip("`* ").isdigit():
|
|
75
|
+
roles.append((c[1].strip("`* "), c[3].strip("`* ")))
|
|
76
|
+
return roles
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def screens_from_uidesign(app, phase):
|
|
80
|
+
pfx = f"{app}-" if phase <= 1 else f"{app}-P{phase}-"
|
|
81
|
+
p = os.path.join("docs", f"{pfx}UIDesign.md")
|
|
82
|
+
out = []
|
|
83
|
+
if os.path.isfile(p):
|
|
84
|
+
for m in re.finditer(r"(?m)^###\s+Screen:\s*(.+?)\s*\(`?([^)`]+)`?\)\s*$", read(p)):
|
|
85
|
+
out.append((m.group(1).strip(), m.group(2).strip()))
|
|
86
|
+
return out, p
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def routes_in_code():
|
|
90
|
+
found = []
|
|
91
|
+
for f in walk(".", (".razor", ".cshtml", ".cs", ".ts", ".tsx", ".js", ".jsx", ".vue")):
|
|
92
|
+
if "/" not in f:
|
|
93
|
+
continue # a top-level file is never a page
|
|
94
|
+
txt = read(f)
|
|
95
|
+
for ext_re, pat, what in ROUTE_PATTERNS:
|
|
96
|
+
if re.search(ext_re, f, re.I):
|
|
97
|
+
for m in pat.finditer(txt):
|
|
98
|
+
r = m.group(1).strip()
|
|
99
|
+
if r and not r.startswith("http") and "[" not in r: # "[controller]/[action]" is a template, not a page
|
|
100
|
+
found.append((r, f, what))
|
|
101
|
+
return found
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def norm_route(r):
|
|
105
|
+
"""'/ManageAccount/{PageId:int}' -> '/manageaccount' : parameters and case never decide a match."""
|
|
106
|
+
r = r.strip().rstrip("/").lower()
|
|
107
|
+
r = re.sub(r"/\{[^}]+\}|/:[a-z_]+", "", r)
|
|
108
|
+
return r or "/"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def sample_files(exts):
|
|
112
|
+
"""Files under the sample folders only (samples/, demos/, apps/, examples/), never the whole tree."""
|
|
113
|
+
out = []
|
|
114
|
+
for d in SAMPLE_DIRS:
|
|
115
|
+
if d == "tests":
|
|
116
|
+
continue
|
|
117
|
+
if os.path.isdir(d):
|
|
118
|
+
out += list(walk(d, exts))
|
|
119
|
+
return out
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def existing_entries(app, phase):
|
|
123
|
+
pfx = f"{app}-" if phase <= 1 else f"{app}-P{phase}-"
|
|
124
|
+
p = os.path.join("docs", f"{pfx}DevGuide.md")
|
|
125
|
+
if not os.path.isfile(p):
|
|
126
|
+
return p, None, []
|
|
127
|
+
txt = read(p)
|
|
128
|
+
m = re.search(r"(?im)^\|\s*Verified on\s*\|\s*([^|]+?)\s*\|", txt)
|
|
129
|
+
return p, (m.group(1).strip() if m else None), re.findall(r"(?m)^###\s+(.+)$", txt)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main(argv):
|
|
133
|
+
if len(argv) < 2 or argv[1] in ("-h", "--help"):
|
|
134
|
+
print(__doc__)
|
|
135
|
+
return 0
|
|
136
|
+
app = argv[1]
|
|
137
|
+
update = "--update" in argv
|
|
138
|
+
phase = int(argv[argv.index("--phase") + 1]) if "--phase" in argv and argv.index("--phase") + 1 < len(argv) else int(cfg("appPhase", "1"))
|
|
139
|
+
kind = cfg("appKind", "app").lower()
|
|
140
|
+
if "--kind" in argv and argv.index("--kind") + 1 < len(argv):
|
|
141
|
+
kind = argv[argv.index("--kind") + 1].lower() # override for a project whose config lacks it
|
|
142
|
+
sub = "app"
|
|
143
|
+
if kind.startswith("lib"):
|
|
144
|
+
razors = [f for f in walk("src", (".razor",), skip_samples=True)] if os.path.isdir("src") else []
|
|
145
|
+
sub = "ui-library" if razors else "service-library"
|
|
146
|
+
print(f"# tf-devguide-list — {app} — kind {sub} — phase {phase}")
|
|
147
|
+
print()
|
|
148
|
+
|
|
149
|
+
if sub == "app":
|
|
150
|
+
screens, uip = screens_from_uidesign(app, phase)
|
|
151
|
+
routes = routes_in_code()
|
|
152
|
+
if not routes:
|
|
153
|
+
print("NOTHING: no routed page found in the code (no @page, [Route], or path: declaration outside samples and tests). Build first.")
|
|
154
|
+
return 0
|
|
155
|
+
roles = roles_from_usageguide(app)
|
|
156
|
+
by_route, endpoints = {}, {}
|
|
157
|
+
for r, f, what in routes:
|
|
158
|
+
if what == "endpoint":
|
|
159
|
+
endpoints.setdefault(norm_route(r), []).append(f)
|
|
160
|
+
else:
|
|
161
|
+
by_route.setdefault(norm_route(r), []).append(f)
|
|
162
|
+
if not by_route:
|
|
163
|
+
print("NOTHING: no routed page found in the code; only API endpoints. Build the screens first.")
|
|
164
|
+
return 0
|
|
165
|
+
print(f"## Work list — {len(by_route)} route(s) in code, {len(screens)} screen(s) in {os.path.basename(uip)}, roles: " + (", ".join(sorted({r for _u, r in roles})) or "none in the UsageGuide"))
|
|
166
|
+
matched = set()
|
|
167
|
+
for name, route in screens:
|
|
168
|
+
key = norm_route(route)
|
|
169
|
+
files = by_route.get(key)
|
|
170
|
+
if files:
|
|
171
|
+
matched.add(key)
|
|
172
|
+
print(f"- {name} ({route}) — {', '.join(sorted(set(files))[:3])}")
|
|
173
|
+
else:
|
|
174
|
+
print(f"- {name} ({route}) — NO CODE FOUND for this route: log it as a defect on the screen's row")
|
|
175
|
+
extra = [k for k in by_route if k not in matched]
|
|
176
|
+
if extra:
|
|
177
|
+
print()
|
|
178
|
+
print("## Pages in code with no UIDesign screen (document them, and tell the analyst)")
|
|
179
|
+
for k in sorted(extra):
|
|
180
|
+
print(f"- {k} — {', '.join(sorted(set(by_route[k]))[:3])}")
|
|
181
|
+
if endpoints:
|
|
182
|
+
print()
|
|
183
|
+
print(f"## API endpoints ({len(endpoints)}; not screens, the call chains end here)")
|
|
184
|
+
for k in sorted(endpoints):
|
|
185
|
+
print(f"- {k} — {', '.join(sorted(set(endpoints[k]))[:2])}")
|
|
186
|
+
elif sub == "ui-library":
|
|
187
|
+
comps = [f for f in walk("src", (".razor",), skip_samples=True)]
|
|
188
|
+
if not comps:
|
|
189
|
+
print("NOTHING: no component under src/. Build first.")
|
|
190
|
+
return 0
|
|
191
|
+
samples = sample_files((".razor", ".cshtml", ".html"))
|
|
192
|
+
stxt = {f: read(f) for f in samples}
|
|
193
|
+
print(f"## Work list — {len(comps)} component(s), grouped by folder; {len(samples)} sample page(s)")
|
|
194
|
+
for f in sorted(comps):
|
|
195
|
+
name = os.path.splitext(os.path.basename(f))[0]
|
|
196
|
+
shown = [s for s, t in stxt.items() if re.search(rf"<{re.escape(name)}\b", t)]
|
|
197
|
+
print(f"- {os.path.dirname(f).split('/')[-1]} / {name} — {f} — " + (f"shown in {shown[0]}" + (f" (+{len(shown)-1})" if len(shown) > 1 else "") if shown else "NO SAMPLE shows it: a sample gap to log"))
|
|
198
|
+
else:
|
|
199
|
+
files = [f for f in walk("src", (".cs", ".ts", ".py", ".go", ".java"), skip_samples=True)]
|
|
200
|
+
if not files:
|
|
201
|
+
print("NOTHING: no source under src/. Build first.")
|
|
202
|
+
return 0
|
|
203
|
+
items = []
|
|
204
|
+
for f in files:
|
|
205
|
+
t = read(f)
|
|
206
|
+
for m in re.finditer(r"public\s+static\s+\w[\w<>,\s]*\s+(Add\w+)\s*\(\s*this\s+", t):
|
|
207
|
+
items.append(("registration", m.group(1), f))
|
|
208
|
+
for m in re.finditer(r"public\s+(?:partial\s+)?interface\s+(I\w+)", t):
|
|
209
|
+
items.append(("interface", m.group(1), f))
|
|
210
|
+
for m in re.finditer(r"public\s+(?:sealed\s+|abstract\s+)?(?:partial\s+)?class\s+(\w+)", t):
|
|
211
|
+
items.append(("class", m.group(1), f))
|
|
212
|
+
samples = sample_files((".cs", ".razor", ".ts", ".py"))
|
|
213
|
+
stxt = {f: read(f) for f in samples}
|
|
214
|
+
items = sorted(set(items), key=lambda x: (x[0] != "registration", x[0] != "interface", x[1])) # overloads once
|
|
215
|
+
print(f"## Work list — {len(items)} public item(s) under src/, {len(samples)} sample file(s)")
|
|
216
|
+
for kind_, name, f in items:
|
|
217
|
+
used = [s for s, t in stxt.items() if re.search(rf"\b{re.escape(name)}\b", t)]
|
|
218
|
+
print(f"- {kind_} {name} — {f} — " + (f"called in {used[0]}" + (f" (+{len(used)-1})" if len(used) > 1 else "") if used else "NO SAMPLE calls it: a sample gap to log"))
|
|
219
|
+
|
|
220
|
+
if update:
|
|
221
|
+
gp, ver, entries = existing_entries(app, phase)
|
|
222
|
+
print()
|
|
223
|
+
if ver is None:
|
|
224
|
+
print(f"## --update: no existing guide at {gp} (or no \"Verified on\"); everything is new")
|
|
225
|
+
else:
|
|
226
|
+
print(f"## --update: existing guide {gp}, Verified on {ver}, {len(entries)} entries")
|
|
227
|
+
try:
|
|
228
|
+
import datetime
|
|
229
|
+
cutoff = datetime.datetime.strptime(ver[:10], "%Y-%m-%d").timestamp()
|
|
230
|
+
except Exception:
|
|
231
|
+
cutoff = None
|
|
232
|
+
if cutoff:
|
|
233
|
+
changed = [f for f in walk(".", (".razor", ".cshtml", ".cs", ".ts", ".tsx", ".js", ".py"), skip_samples=False)
|
|
234
|
+
if f.startswith(("src/",)) and os.path.getmtime(f) > cutoff]
|
|
235
|
+
print(f"- {len(changed)} source file(s) changed since then; remap the entries they serve, keep the rest verbatim")
|
|
236
|
+
for f in changed[:30]:
|
|
237
|
+
print(f" - {f}")
|
|
238
|
+
return 0
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == "__main__":
|
|
242
|
+
try:
|
|
243
|
+
sys.exit(main(sys.argv))
|
|
244
|
+
except SystemExit:
|
|
245
|
+
raise
|
|
246
|
+
except BrokenPipeError:
|
|
247
|
+
sys.exit(0) # piped into head: not an error
|
|
248
|
+
except Exception as e:
|
|
249
|
+
die(str(e))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-devguide-list.sh — the work list for *devguide.
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-devguide-list.sh MyApp # app: routes in code vs UIDesign screens, roles
|
|
5
|
+
# bash .tfcore/utils/tf-devguide-list.sh MyApp --update # plus which existing entries changed
|
|
6
|
+
# bash .tfcore/utils/tf-devguide-list.sh MyApp --phase 2 # a later phase of a Large project
|
|
7
|
+
#
|
|
8
|
+
# Kind comes from core-config.yaml: an app lists routed pages; a UI library its public
|
|
9
|
+
# components and the sample page that shows each; a service library its registrations,
|
|
10
|
+
# interfaces and classes and the sample that calls each. Prints NOTHING when no code exists.
|
|
11
|
+
# Writes nothing. Thin wrapper over tf-devguide-list.py; Python 3 stdlib 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-devguide-list: python3 is required (standard library only)." >&2
|
|
16
|
+
exit 2
|
|
17
|
+
fi
|
|
18
|
+
exec python3 "$SELF_DIR/tf-devguide-list.py" "$@"
|