@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.
Files changed (174) hide show
  1. package/.claude/commands/TechieFlow/agents/analyst.md +0 -5
  2. package/.claude/commands/TechieFlow/agents/architect.md +0 -6
  3. package/.claude/commands/TechieFlow/agents/flow-master.md +41 -215
  4. package/.claude/commands/TechieFlow/agents/verifier.md +23 -64
  5. package/.claude/commands/TechieFlow/tasks/_metrics-emit-gate.md +24 -117
  6. package/.claude/commands/TechieFlow/tasks/_smoke-test-policy.md +20 -84
  7. package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +10 -74
  8. package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +20 -58
  9. package/.claude/commands/TechieFlow/tasks/amend-docs.md +20 -143
  10. package/.claude/commands/TechieFlow/tasks/build-phase.md +14 -224
  11. package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +31 -505
  12. package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +28 -213
  13. package/.claude/commands/TechieFlow/tasks/deploy-checklist.md +19 -0
  14. package/.claude/commands/TechieFlow/tasks/devguide.md +10 -219
  15. package/.claude/commands/TechieFlow/tasks/fix-issues.md +10 -129
  16. package/.claude/commands/TechieFlow/tasks/log-miss.md +19 -158
  17. package/.claude/commands/TechieFlow/tasks/metrics-report.md +1 -1
  18. package/.claude/commands/TechieFlow/tasks/mockups.md +12 -117
  19. package/.claude/commands/TechieFlow/tasks/refresh-status.md +9 -130
  20. package/.claude/commands/TechieFlow/tasks/split-brd.md +10 -126
  21. package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +14 -0
  22. package/.claude/commands/TechieFlow/tasks/triage-issues.md +11 -152
  23. package/.claude/commands/TechieFlow/tasks/verify-phase.md +36 -572
  24. package/.opencode/plugin/techieflow.js +39 -12
  25. package/.tfcore/agents/analyst.md +0 -5
  26. package/.tfcore/agents/architect.md +0 -6
  27. package/.tfcore/agents/flow-master.md +41 -215
  28. package/.tfcore/agents/verifier.md +23 -64
  29. package/.tfcore/core-config.yaml +10 -0
  30. package/.tfcore/hooks/block-git.sh +4 -4
  31. package/.tfcore/hooks/guard-artifacts.sh +2 -2
  32. package/.tfcore/hooks/guard-build.sh +76 -0
  33. package/.tfcore/hooks/guard-db.sh +90 -0
  34. package/.tfcore/hooks/guard-metrics.sh +56 -0
  35. package/.tfcore/hooks/guard-status-html.sh +116 -71
  36. package/.tfcore/hooks/guard-status.sh +43 -16
  37. package/.tfcore/hooks/guard-verify.sh +70 -48
  38. package/.tfcore/hooks/sweep-artifacts.sh +2 -2
  39. package/.tfcore/install-manifest.yaml +0 -18
  40. package/.tfcore/routing.yaml +25 -14
  41. package/.tfcore/standards/coding-standards-core.md +61 -0
  42. package/.tfcore/standards/coding-standards-dotnet.md +93 -0
  43. package/.tfcore/tasks/_metrics-emit-gate.md +24 -117
  44. package/.tfcore/tasks/_smoke-test-policy.md +20 -84
  45. package/.tfcore/tasks/_status-update-gate.md +10 -74
  46. package/.tfcore/tasks/_yolo-mode.md +20 -58
  47. package/.tfcore/tasks/amend-docs.md +20 -143
  48. package/.tfcore/tasks/build-phase.md +14 -224
  49. package/.tfcore/tasks/day1-brownfield.md +31 -505
  50. package/.tfcore/tasks/day1-greenfield.md +28 -213
  51. package/.tfcore/tasks/deploy-checklist.md +19 -0
  52. package/.tfcore/tasks/devguide.md +10 -219
  53. package/.tfcore/tasks/fix-issues.md +10 -129
  54. package/.tfcore/tasks/log-miss.md +19 -158
  55. package/.tfcore/tasks/metrics-report.md +1 -1
  56. package/.tfcore/tasks/mockups.md +12 -117
  57. package/.tfcore/tasks/refresh-status.md +9 -130
  58. package/.tfcore/tasks/split-brd.md +10 -126
  59. package/.tfcore/tasks/triage-and-fix.md +14 -0
  60. package/.tfcore/tasks/triage-issues.md +11 -152
  61. package/.tfcore/tasks/verify-phase.md +36 -572
  62. package/.tfcore/telemetry/SCHEMA.md +43 -7
  63. package/.tfcore/telemetry/tf-metrics.sh +152 -17
  64. package/.tfcore/templates/project-brief-tmpl.yaml +1 -1
  65. package/.tfcore/templates/stack-defaults/dotnet.md +35 -0
  66. package/.tfcore/templates/stack-questions.md +77 -0
  67. package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +3 -4
  68. package/.tfcore/templates/v4custom/app-architecture-tmpl.md +107 -115
  69. package/.tfcore/templates/v4custom/app-brd-tmpl.md +108 -184
  70. package/.tfcore/templates/v4custom/app-checklist-tmpl.md +48 -73
  71. package/.tfcore/templates/v4custom/app-claude-md-tmpl.md +1 -1
  72. package/.tfcore/templates/v4custom/app-coding-standards-tmpl.md +52 -0
  73. package/.tfcore/templates/v4custom/app-deployment-checklist-tmpl.md +86 -0
  74. package/.tfcore/templates/v4custom/app-devguide-tmpl.md +69 -146
  75. package/.tfcore/templates/v4custom/app-phases-tmpl.md +35 -0
  76. package/.tfcore/templates/v4custom/app-productguide-tmpl.md +38 -35
  77. package/.tfcore/templates/v4custom/app-project-status-tmpl.md +63 -73
  78. package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +62 -37
  79. package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +69 -54
  80. package/.tfcore/templates/v4custom/build-invocation-ladder.md +17 -180
  81. package/.tfcore/templates/v4custom/build-subagent-prompt.md +17 -0
  82. package/.tfcore/templates/v4custom/metrics-report-template.md +2 -2
  83. package/.tfcore/user-guide.md +0 -23
  84. package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
  85. package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
  86. package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
  87. package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
  88. package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
  89. package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
  90. package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
  91. package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
  92. package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
  93. package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
  94. package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
  95. package/.tfcore/utils/__pycache__/tf-perf-grade.cpython-312.pyc +0 -0
  96. package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
  97. package/.tfcore/utils/__pycache__/tf-split-brd.cpython-312.pyc +0 -0
  98. package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
  99. package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
  100. package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
  101. package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
  102. package/.tfcore/utils/tf-brd-status.py +176 -0
  103. package/.tfcore/utils/tf-brd-status.sh +19 -0
  104. package/.tfcore/utils/tf-build-list.py +191 -0
  105. package/.tfcore/utils/tf-build-list.sh +20 -0
  106. package/.tfcore/utils/tf-build.sh +143 -0
  107. package/.tfcore/utils/tf-cdp-relay.ps1 +64 -0
  108. package/.tfcore/utils/tf-checklist-edit.py +133 -0
  109. package/.tfcore/utils/tf-day1-files.py +208 -0
  110. package/.tfcore/utils/tf-day1-files.sh +20 -0
  111. package/.tfcore/utils/tf-devguide-list.py +249 -0
  112. package/.tfcore/utils/tf-devguide-list.sh +18 -0
  113. package/.tfcore/utils/tf-doc-check.py +1267 -0
  114. package/.tfcore/utils/tf-doc-check.sh +29 -0
  115. package/.tfcore/utils/tf-emit.sh +203 -13
  116. package/.tfcore/utils/tf-fix-close.sh +83 -0
  117. package/.tfcore/utils/tf-goal.sh +193 -62
  118. package/.tfcore/utils/tf-harness.sh +4 -9
  119. package/.tfcore/utils/tf-log-miss.py +197 -0
  120. package/.tfcore/utils/tf-log-miss.sh +14 -0
  121. package/.tfcore/utils/tf-misses-md.py +211 -0
  122. package/.tfcore/utils/tf-misses-md.sh +13 -0
  123. package/.tfcore/utils/tf-mockups-locate.py +66 -0
  124. package/.tfcore/utils/tf-mockups-locate.sh +18 -0
  125. package/.tfcore/utils/tf-opencode-audit.py +23 -7
  126. package/.tfcore/utils/tf-perf-grade.py +97 -0
  127. package/.tfcore/utils/tf-perf-grade.sh +12 -0
  128. package/.tfcore/utils/tf-phase.sh +74 -0
  129. package/.tfcore/utils/tf-routing-bind.sh +10 -5
  130. package/.tfcore/utils/tf-routing.sh +6 -7
  131. package/.tfcore/utils/tf-split-brd.py +304 -0
  132. package/.tfcore/utils/tf-split-brd.sh +22 -0
  133. package/.tfcore/utils/tf-status-evidence.sh +68 -0
  134. package/.tfcore/utils/tf-status-facts.py +309 -0
  135. package/.tfcore/utils/tf-status-facts.sh +20 -0
  136. package/.tfcore/utils/tf-triage.py +243 -0
  137. package/.tfcore/utils/tf-triage.sh +16 -0
  138. package/.tfcore/utils/tf-verify-boot.sh +250 -0
  139. package/.tfcore/utils/tf-verify-emit.sh +110 -0
  140. package/.tfcore/utils/tf-verify-env.sh +88 -0
  141. package/.tfcore/utils/tf-verify-list.py +270 -0
  142. package/.tfcore/utils/tf-verify-list.sh +13 -0
  143. package/.tfcore/utils/tf-verify-screens.mjs +283 -0
  144. package/.tfcore/utils/tf-verify-screens.sh +22 -0
  145. package/.tfcore/utils/tf-verify-tests.sh +123 -0
  146. package/.tfcore/utils/tf-verify-verdict.py +283 -0
  147. package/.tfcore/utils/tf-verify-verdict.sh +15 -0
  148. package/.tfcore/utils/tf-yolo.sh +59 -0
  149. package/README.md +114 -1149
  150. package/docs/TechieFlow-Installation.md +1 -5
  151. package/opencode.jsonc +13 -33
  152. package/package.json +1 -5
  153. package/scripts/install.mjs +52 -46
  154. package/.claude/commands/TechieFlow/tasks/advanced-elicitation.md +0 -117
  155. package/.claude/commands/TechieFlow/tasks/author-brd.md +0 -79
  156. package/.claude/commands/TechieFlow/tasks/document-project.md +0 -343
  157. package/.claude/commands/TechieFlow/tasks/execute-checklist.md +0 -86
  158. package/.claude/commands/TechieFlow/tasks/index-docs.md +0 -173
  159. package/.claude/commands/TechieFlow/tasks/kb-mode-interaction.md +0 -75
  160. package/.claude/commands/TechieFlow/tasks/shard-doc.md +0 -185
  161. package/.codex/config.toml +0 -46
  162. package/.codex/hooks.json +0 -63
  163. package/.codex/rules/techieflow.rules +0 -8
  164. package/.tfcore/hooks/codex-adapter.py +0 -248
  165. package/.tfcore/tasks/advanced-elicitation.md +0 -117
  166. package/.tfcore/tasks/author-brd.md +0 -79
  167. package/.tfcore/tasks/document-project.md +0 -343
  168. package/.tfcore/tasks/execute-checklist.md +0 -86
  169. package/.tfcore/tasks/index-docs.md +0 -173
  170. package/.tfcore/tasks/kb-mode-interaction.md +0 -75
  171. package/.tfcore/tasks/shard-doc.md +0 -185
  172. package/.tfcore/utils/tf-codex-bind.py +0 -196
  173. package/.tfcore/utils/tf-codex-telemetry.py +0 -60
  174. 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" "$@"