@techierathore/techieflow 1.0.4 → 1.1.1
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/flow-master.md +2 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +2 -2
- package/.claude/commands/TechieFlow/tasks/_develop-mode.md +34 -0
- package/.claude/commands/TechieFlow/tasks/_owner-language.md +1 -0
- package/.claude/commands/TechieFlow/tasks/_status-update-gate.md +4 -3
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +2 -2
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +3 -3
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +5 -5
- package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +3 -2
- package/.claude/commands/TechieFlow/tasks/generate-html.md +2 -2
- package/.claude/commands/TechieFlow/tasks/handoff-phase.md +2 -2
- package/.claude/commands/TechieFlow/tasks/mockups.md +2 -2
- package/.claude/commands/TechieFlow/tasks/productguide.md +2 -2
- package/.claude/commands/TechieFlow/tasks/render-workflow-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
- package/.claude/commands/TechieFlow/tasks/triage-issues.md +2 -2
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
- package/.opencode/plugin/techieflow.js +194 -14
- package/.tfcore/agents/flow-master.md +2 -0
- package/.tfcore/agents/verifier.md +2 -2
- package/.tfcore/core-config.yaml +2 -1
- package/.tfcore/hooks/block-git.sh +4 -2
- package/.tfcore/hooks/guard-build.sh +5 -1
- package/.tfcore/hooks/guard-db.sh +112 -13
- package/.tfcore/hooks/guard-status-html.sh +86 -2
- package/.tfcore/hooks/guard-verify-deps.sh +16 -1
- package/.tfcore/hooks/guard-verify.sh +8 -3
- package/.tfcore/hooks/sweep-artifacts.sh +46 -0
- package/.tfcore/hooks/yolo-allow.sh +27 -0
- package/.tfcore/standards/blazor-zoom.cs.txt +191 -0
- package/.tfcore/standards/coding-standards-dotnet.md +31 -3
- package/.tfcore/standards/owner-words.txt +23 -0
- package/.tfcore/tasks/_develop-mode.md +34 -0
- package/.tfcore/tasks/_owner-language.md +1 -0
- package/.tfcore/tasks/_status-update-gate.md +4 -3
- package/.tfcore/tasks/_yolo-mode.md +2 -2
- package/.tfcore/tasks/amend-docs.md +2 -2
- package/.tfcore/tasks/day1-brownfield.md +3 -3
- package/.tfcore/tasks/day1-greenfield.md +5 -5
- package/.tfcore/tasks/develop-end-to-end.md +30 -0
- package/.tfcore/tasks/fix-issues.md +3 -2
- package/.tfcore/tasks/generate-html.md +2 -2
- package/.tfcore/tasks/handoff-phase.md +2 -2
- package/.tfcore/tasks/mockups.md +2 -2
- package/.tfcore/tasks/productguide.md +2 -2
- package/.tfcore/tasks/render-workflow-docs.md +1 -1
- package/.tfcore/tasks/triage-and-fix.md +2 -2
- package/.tfcore/tasks/triage-issues.md +2 -2
- package/.tfcore/tasks/verify-phase.md +12 -11
- package/.tfcore/telemetry/SCHEMA.md +10 -2
- package/.tfcore/telemetry/install-metrics.sh +3 -2
- package/.tfcore/telemetry/tf-metrics.sh +38 -2
- package/.tfcore/templates/stack-defaults/dotnet.md +3 -1
- package/.tfcore/templates/v4custom/app-agents-md-tmpl.md +6 -0
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +7 -4
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
- package/.tfcore/templates/v4custom/app-phase-brd-tmpl.md +94 -0
- package/.tfcore/templates/v4custom/app-phase-uidesign-tmpl.md +60 -0
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
- package/.tfcore/templates/v4custom/app-uidesign-tmpl.md +4 -2
- package/.tfcore/templates/v4custom/build-subagent-prompt.md +2 -2
- package/.tfcore/templates/v4custom/html-render-shell.md +6 -2
- 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-split-brd.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-status-facts.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-appium.mjs +179 -0
- package/.tfcore/utils/tf-assets-browser.mjs +96 -0
- package/.tfcore/utils/tf-assets.sh +97 -12
- package/.tfcore/utils/tf-brd-status.py +14 -5
- package/.tfcore/utils/tf-build-list.py +95 -12
- package/.tfcore/utils/tf-build-list.sh +2 -1
- package/.tfcore/utils/tf-build.sh +163 -15
- package/.tfcore/utils/tf-checklist-edit.py +21 -3
- package/.tfcore/utils/tf-ci-repro.py +685 -0
- package/.tfcore/utils/tf-ci-repro.sh +24 -0
- package/.tfcore/utils/tf-day1-files.py +55 -28
- package/.tfcore/utils/tf-develop.sh +264 -0
- package/.tfcore/utils/tf-devguide-list.py +53 -24
- package/.tfcore/utils/tf-doc-check.py +218 -26
- package/.tfcore/utils/tf-doc-tests.sh +116 -0
- package/.tfcore/utils/tf-emit.sh +132 -45
- package/.tfcore/utils/tf-feedback.sh +13 -0
- package/.tfcore/utils/tf-fix-close.sh +87 -31
- package/.tfcore/utils/tf-goal.sh +12 -12
- package/.tfcore/utils/tf-harness.sh +9 -4
- package/.tfcore/utils/tf-lock.sh +29 -0
- package/.tfcore/utils/tf-log-miss.py +34 -8
- package/.tfcore/utils/tf-login.mjs +167 -0
- package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
- package/.tfcore/utils/tf-menus.mjs +401 -0
- package/.tfcore/utils/tf-misses-md.py +10 -4
- package/.tfcore/utils/tf-mockup-parity.mjs +763 -71
- package/.tfcore/utils/tf-mockup-parity.sh +25 -2
- package/.tfcore/utils/tf-owner-text.py +233 -0
- package/.tfcore/utils/tf-owner-text.sh +14 -0
- package/.tfcore/utils/tf-phase.sh +38 -4
- package/.tfcore/utils/tf-portable.sh +230 -0
- package/.tfcore/utils/tf-render-html.py +79 -5
- package/.tfcore/utils/tf-req-ids.py +36 -0
- package/.tfcore/utils/tf-selfcheck.sh +39 -6
- package/.tfcore/utils/tf-split-brd.py +11 -1
- package/.tfcore/utils/tf-stack-check.sh +3 -0
- package/.tfcore/utils/tf-status-evidence.sh +11 -9
- package/.tfcore/utils/tf-status-facts.py +160 -45
- package/.tfcore/utils/tf-theme.mjs +189 -0
- package/.tfcore/utils/tf-triage.py +74 -17
- package/.tfcore/utils/tf-triage.sh +2 -0
- package/.tfcore/utils/tf-verify-boot.sh +529 -54
- package/.tfcore/utils/tf-verify-emit.sh +57 -17
- package/.tfcore/utils/tf-verify-env.sh +33 -4
- package/.tfcore/utils/tf-verify-list.py +290 -24
- package/.tfcore/utils/tf-verify-list.sh +3 -1
- package/.tfcore/utils/tf-verify-native.mjs +307 -0
- package/.tfcore/utils/tf-verify-screens.mjs +341 -44
- package/.tfcore/utils/tf-verify-screens.sh +23 -5
- package/.tfcore/utils/tf-verify-tests.sh +310 -19
- package/.tfcore/utils/tf-verify-verdict.py +163 -21
- package/.tfcore/utils/tf-verify-zoom.mjs +193 -0
- package/.tfcore/utils/tf-verify-zoom.sh +29 -0
- package/.tfcore/utils/tf-yolo-alias.sh +84 -0
- package/.tfcore/utils/tf-yolo-perms.py +135 -0
- package/.tfcore/utils/tf-yolo-start.sh +44 -0
- package/.tfcore/utils/tf-yolo.sh +31 -3
- package/.tfcore/utils/tf_defect.py +27 -0
- package/.tfcore/utils/tf_feedback.py +272 -0
- package/.tfcore/utils/tf_roadmap.py +35 -0
- package/.tfcore/utils/tf_stack_check.py +93 -0
- package/README.md +6 -2
- package/docs/TechieFlow-Installation.md +5 -3
- package/opencode.jsonc +6 -2
- package/package.json +1 -1
- package/scripts/install.mjs +13 -2
- package/.tfcore/utils/__pycache__/tf-brd-status.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-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-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-ci-repro.sh — reproduce a CI job here the way a fresh runner runs it (Lekhak TF-019, 2026-09-30).
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-ci-repro.sh [<workflow.yml>] [--job <id>] [--list] [--keep] [--run-setup]
|
|
5
|
+
#
|
|
6
|
+
# Run from the repository root. A developer machine hides CI failures in its package caches and its
|
|
7
|
+
# build output: Lekhak's CI failed with NETSDK1112 because the win-x64 runtime pack was never
|
|
8
|
+
# downloaded, while the same commands passed locally where the pack was already cached. A local run
|
|
9
|
+
# with warm caches is therefore no proof that a CI failure is fixed. This script copies the repository
|
|
10
|
+
# without anything .gitignore ignores, to a folder outside it, points every package cache it knows
|
|
11
|
+
# (NuGet, npm, yarn, pnpm, pip, Go, Gradle, Maven) at an empty folder, and runs the job's `run:` steps
|
|
12
|
+
# in the runner's shell, stopping at the first failure. A Windows job runs Windows-side from WSL.
|
|
13
|
+
# `uses:` steps, steps that need a secret and steps that only install tools on the machine are
|
|
14
|
+
# skipped, each with the reason. --list prints the plan and runs nothing; --keep keeps the copy.
|
|
15
|
+
# Prints ONE verdict line last:
|
|
16
|
+
# PASS job <id>: <n> run step(s) passed on a clean copy with empty caches (…) — logs <dir> exit 0
|
|
17
|
+
# FAIL job <id>: step <n> "<name>" failed (exit N) with empty caches — <first error> — log <file> exit 1
|
|
18
|
+
# NOT-RUN <why: no workflow, several jobs, a macOS job off a Mac …> exit 2
|
|
19
|
+
# PyYAML is used when installed; without it (a stock Mac) a built-in reader takes the workflow.
|
|
20
|
+
# Logs: tests/.artifacts/ci-repro/<UTC time>/step-NN.log. The first run downloads every package, so
|
|
21
|
+
# it takes as long as a cold CI restore.
|
|
22
|
+
set -uo pipefail
|
|
23
|
+
case "${1:-}" in -h|--help) sed -n '2,21p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; esac
|
|
24
|
+
exec python3 "$(dirname "${BASH_SOURCE[0]}")/tf-ci-repro.py" "$@"
|
|
@@ -4,15 +4,17 @@
|
|
|
4
4
|
bash .tfcore/utils/tf-day1-files.sh <App> [--size S|M|L] [--kind app|library] [--prefix obj|none] [--phase N]
|
|
5
5
|
bash .tfcore/utils/tf-day1-files.sh --archive <file> [<file> ...]
|
|
6
6
|
|
|
7
|
-
Default mode, for <App
|
|
8
|
-
- .tfcore/core-config.yaml
|
|
9
|
-
|
|
7
|
+
Default mode, for <App>. Each flag does its own part and nothing else:
|
|
8
|
+
- --size, --kind, --phase set appSize, appKind, appPhase in .tfcore/core-config.yaml; --phase is the
|
|
9
|
+
phase being worked (handoff moves it on).
|
|
10
10
|
- --size L also writes docs/<App>-Phases.md from app-phases-tmpl.md when it does not exist
|
|
11
11
|
(the Large layout: docs/TechieFlow-Document-Schemas.md §2, §3.11). The agent fills the table.
|
|
12
|
-
- --
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
- --prefix obj|none (day-1 stage 2) writes .editorconfig (overwritten; machine config), AGENTS.md and
|
|
13
|
+
CLAUDE.md from their templates with {AppName} substituted and the field-prefix line resolved, an
|
|
14
|
+
existing copy archived first; and adds the brd, architecture and codingStandards paths to
|
|
15
|
+
customTechnicalDocuments and devLoadAlwaysFiles where missing, keeping every entry already there.
|
|
16
|
+
Raising the size of a project with *amend-docs (--size L) once replaced Sevak's AGENTS.md and CLAUDE.md
|
|
17
|
+
with blank templates and cut its document list to three entries (Sevak TF-001). tests/regression sv_001.
|
|
16
18
|
--archive: move each file (and its sibling .html) to docs/OldDocs/, date-suffixed when a
|
|
17
19
|
file of that name is already there. Never asks; never writes a -v2 variant.
|
|
18
20
|
|
|
@@ -86,22 +88,45 @@ def set_key(text, key, value_lines):
|
|
|
86
88
|
return "\n".join(out) + "\n"
|
|
87
89
|
|
|
88
90
|
|
|
89
|
-
def
|
|
91
|
+
def block_of(text, key):
|
|
92
|
+
"""The indented lines under a top-level YAML key, [] when the key is absent or empty."""
|
|
93
|
+
lines, out, inside = text.splitlines(), [], False
|
|
94
|
+
for line in lines:
|
|
95
|
+
if re.match(rf"^{re.escape(key)}:", line):
|
|
96
|
+
inside = True
|
|
97
|
+
continue
|
|
98
|
+
if inside:
|
|
99
|
+
if line.startswith((" ", "\t")):
|
|
100
|
+
out.append(line)
|
|
101
|
+
elif line.strip():
|
|
102
|
+
break
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def add_documents(s, app):
|
|
107
|
+
"""The day-1 document paths, added where missing; every entry already listed is kept."""
|
|
108
|
+
docs = block_of(s, "customTechnicalDocuments")
|
|
109
|
+
have = {m.group(1) for m in (re.match(r"^\s+([\w-]+):", l) for l in docs) if m}
|
|
110
|
+
for k, v in (("brd", f"docs/{app}-BRD.md"), ("architecture", f"docs/{app}-Architecture.md"),
|
|
111
|
+
("codingStandards", f"docs/{app}-Coding-Standards.md")):
|
|
112
|
+
if k not in have:
|
|
113
|
+
docs.append(f" {k}: {v}")
|
|
114
|
+
s = set_key(s, "customTechnicalDocuments", ["customTechnicalDocuments:"] + docs)
|
|
115
|
+
files = block_of(s, "devLoadAlwaysFiles")
|
|
116
|
+
listed = {l.strip().lstrip("-").strip() for l in files}
|
|
117
|
+
for f in (f"docs/{app}-Coding-Standards.md", f"docs/{app}-Architecture.md"):
|
|
118
|
+
if f not in listed:
|
|
119
|
+
files.append(f" - {f}")
|
|
120
|
+
return set_key(s, "devLoadAlwaysFiles", ["devLoadAlwaysFiles:"] + files)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def update_config(app, size, kind, phase, documents):
|
|
90
124
|
p = os.path.join(".tfcore", "core-config.yaml")
|
|
91
125
|
if not os.path.isfile(p):
|
|
92
126
|
die(f"{p} does not exist at its literal path; is the framework installed here?")
|
|
93
127
|
s = read(p)
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
])
|
|
128
|
+
if documents:
|
|
129
|
+
s = add_documents(s, app)
|
|
105
130
|
if size:
|
|
106
131
|
s = set_key(s, "appSize", [f"appSize: {size}"])
|
|
107
132
|
if kind:
|
|
@@ -109,9 +134,9 @@ def update_config(app, size, kind, phase):
|
|
|
109
134
|
if phase:
|
|
110
135
|
s = set_key(s, "appPhase", [f"appPhase: {phase}"])
|
|
111
136
|
write(p, s)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
137
|
+
done = ([f"documents for {app}"] if documents else []) + ([f"appSize {size}"] if size else []) \
|
|
138
|
+
+ ([f"appKind {kind}"] if kind else []) + ([f"appPhase {phase}"] if phase else [])
|
|
139
|
+
print(f"tf-day1-files: .tfcore/core-config.yaml updated ({', '.join(done)})")
|
|
115
140
|
|
|
116
141
|
|
|
117
142
|
def write_phases_doc(app):
|
|
@@ -161,8 +186,7 @@ def main(argv):
|
|
|
161
186
|
app = argv[1]
|
|
162
187
|
if not re.fullmatch(r"[A-Z][A-Za-z0-9]*", app):
|
|
163
188
|
die(f"app name {app!r} must be PascalCase with no spaces")
|
|
164
|
-
size = kind = phase = None
|
|
165
|
-
prefix = "obj"
|
|
189
|
+
size = kind = phase = prefix = None
|
|
166
190
|
args = argv[2:]
|
|
167
191
|
i = 0
|
|
168
192
|
while i < len(args):
|
|
@@ -190,12 +214,15 @@ def main(argv):
|
|
|
190
214
|
i += 2
|
|
191
215
|
else:
|
|
192
216
|
die(f"unknown argument {a!r}")
|
|
193
|
-
|
|
217
|
+
if not (size or kind or phase or prefix):
|
|
218
|
+
die("nothing to do: give --size, --kind, --phase or --prefix")
|
|
219
|
+
update_config(app, size, kind, phase, documents=bool(prefix))
|
|
194
220
|
if size == "L":
|
|
195
221
|
write_phases_doc(app)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
222
|
+
if prefix: # day-1 stage 2 only: a size, kind or phase change never rewrites these (Sevak TF-001)
|
|
223
|
+
write_from_template(app, "app-editorconfig-tmpl.editorconfig", ".editorconfig", None)
|
|
224
|
+
write_from_template(app, "app-agents-md-tmpl.md", "AGENTS.md", prefix)
|
|
225
|
+
write_from_template(app, "app-claude-md-tmpl.md", "CLAUDE.md", prefix)
|
|
199
226
|
return 0
|
|
200
227
|
|
|
201
228
|
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tf-develop.sh — *develop-end-to-end: a brief in, a greenfield app out, up to UAT, unattended (2026-10-08).
|
|
3
|
+
#
|
|
4
|
+
# bash .tfcore/utils/tf-develop.sh <app-dir> --app <App> --brief <file> [--harness claude|opencode]
|
|
5
|
+
# [--model <id>] [--tier standard] [--no-push] [--dry-run] [--detach]
|
|
6
|
+
# bash .tfcore/utils/tf-develop.sh <app-dir> --report # write the Build Report again from the saved state
|
|
7
|
+
# bash .tfcore/utils/tf-develop.sh <app-dir> --resume # continue a stopped run where it stopped
|
|
8
|
+
#
|
|
9
|
+
# The flow-master's *develop-end-to-end starts this; the owner never types it. It runs four phases, each
|
|
10
|
+
# one an unattended goal run (tf-goal.sh: no questions, survives usage limits by waiting for the reset
|
|
11
|
+
# and resuming the same session):
|
|
12
|
+
# day1 *day1-greenfield <App> BRD, Architecture, mockups — no owner review stop
|
|
13
|
+
# day1-2 *day1-greenfield <App> --stage2 checklist and the remaining documents
|
|
14
|
+
# build *build-phase <App> every row built, verified inline, FIX mode on failures
|
|
15
|
+
# handoff *handoff-phase <App> UsageGuide with its smoke checklist and test users: UAT-ready
|
|
16
|
+
# While it runs, .tfcore/.session/develop.json exists and every task follows .tfcore/tasks/_develop-mode.md.
|
|
17
|
+
#
|
|
18
|
+
# GIT — the one exception to "agents never write to git", for this command only. The agent still
|
|
19
|
+
# cannot commit: its git guard is unchanged. THIS script commits after each phase, and pushes, from
|
|
20
|
+
# its own shell, never through an agent's tool. When it ends, nothing is left switched on.
|
|
21
|
+
#
|
|
22
|
+
# METRICS — after every phase it writes docs/metrics/develop-report.json and docs/<App>-Build-Report.md:
|
|
23
|
+
# per phase the wall-clock time (limit waits included), the agent's working time and its tokens (summed
|
|
24
|
+
# from that phase's run records in docs/metrics/runs.jsonl), and the totals: "built in …".
|
|
25
|
+
#
|
|
26
|
+
# Exit 0 UAT-ready · 3 the agent declared a phase BLOCKED (the owner must act; listed in the report)
|
|
27
|
+
# · 4 a phase ran out of cycles · 5 the provider refused the model · 2 usage error.
|
|
28
|
+
set -uo pipefail
|
|
29
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
30
|
+
case "${1:-}" in
|
|
31
|
+
-h|--help) sed -n '2,27p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
32
|
+
"") sed -n '2,27p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
|
|
33
|
+
esac
|
|
34
|
+
|
|
35
|
+
DIR="$(cd "$1" 2>/dev/null && pwd)" || { echo "tf-develop: no such dir: $1" >&2; exit 2; }; shift
|
|
36
|
+
APP=""; BRIEF=""; HARNESS="claude"; MODEL=""; TIER="standard"; PUSH=1; DRY=0; RESUME=0; DETACH=0; REPORT_ONLY=0
|
|
37
|
+
ARGS_IN=("$@")
|
|
38
|
+
while [[ $# -gt 0 ]]; do
|
|
39
|
+
case "$1" in
|
|
40
|
+
--app) APP="${2:-}"; shift 2 ;;
|
|
41
|
+
--brief) BRIEF="${2:-}"; shift 2 ;;
|
|
42
|
+
--harness) HARNESS="${2:-}"; shift 2 ;;
|
|
43
|
+
--model) MODEL="${2:-}"; shift 2 ;;
|
|
44
|
+
--tier) TIER="${2:-}"; shift 2 ;;
|
|
45
|
+
--no-push) PUSH=0; shift ;;
|
|
46
|
+
--dry-run) DRY=1; shift ;;
|
|
47
|
+
--resume) RESUME=1; shift ;;
|
|
48
|
+
--detach) DETACH=1; shift ;;
|
|
49
|
+
--report) REPORT_ONLY=1; shift ;;
|
|
50
|
+
*) echo "tf-develop: unknown argument $1" >&2; exit 2 ;;
|
|
51
|
+
esac
|
|
52
|
+
done
|
|
53
|
+
# --detach: start this same run in its own session and return at once, so it outlives the agent session
|
|
54
|
+
# that started it (the flow-master's *develop-end-to-end), in a session of its own (tf_setsid, portable).
|
|
55
|
+
if [[ $DETACH -eq 1 ]]; then
|
|
56
|
+
mkdir -p "$DIR/.tfcore/.session"
|
|
57
|
+
rest=(); for a in "${ARGS_IN[@]}"; do [[ "$a" != --detach ]] && rest+=("$a"); done
|
|
58
|
+
source "$HERE/tf-portable.sh" # tf_setsid works on macOS too (a python3 or perl stand-in)
|
|
59
|
+
tf_setsid bash "$0" "$DIR" "${rest[@]}" > "$DIR/.tfcore/.session/develop.out" 2>&1 < /dev/null &
|
|
60
|
+
echo "tf-develop: started (pid $!) — follow .tfcore/.session/develop.log; the result is docs/<App>-Build-Report.md"
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
63
|
+
STATE="$DIR/.tfcore/.session/develop.json"
|
|
64
|
+
GOAL_SH="${TF_DEVELOP_GOAL_SH:-$DIR/.tfcore/utils/tf-goal.sh}" # tests point this at a fake
|
|
65
|
+
mkdir -p "$DIR/.tfcore/.session" "$DIR/docs/metrics"
|
|
66
|
+
ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
|
67
|
+
# The app folder must be the TOP of its own repository. A folder inside another repository answers
|
|
68
|
+
# rev-parse too, and `git add -A` there staged the whole outer repository (found by dev_001 on its first run).
|
|
69
|
+
own_repo() { local top; top="$(git -C "$DIR" rev-parse --show-toplevel 2>/dev/null)" || return 1
|
|
70
|
+
[[ -n "$top" && "$(cd "$top" && pwd -P)" == "$(cd "$DIR" && pwd -P)" ]]; }
|
|
71
|
+
log() { printf '[%s] tf-develop: %s\n' "$(ts)" "$*" | tee -a "$DIR/.tfcore/.session/develop.log" >&2; }
|
|
72
|
+
|
|
73
|
+
st() { python3 - "$STATE" "$@" <<'PY'
|
|
74
|
+
import json, sys
|
|
75
|
+
p, op, *a = sys.argv[1:]
|
|
76
|
+
try: d = json.load(open(p))
|
|
77
|
+
except Exception: d = {}
|
|
78
|
+
if op == "get": print(d.get(a[0], ""))
|
|
79
|
+
elif op == "init":
|
|
80
|
+
d = {"app": a[0], "brief": a[1], "harness": a[2], "model": a[3], "tier": a[4], "push": a[5] == "1", "started": a[6], "phases": []}
|
|
81
|
+
elif op == "done": print("yes" if any(x["name"] == a[0] and x["status"] == "done" for x in d.get("phases", [])) else "")
|
|
82
|
+
elif op == "attempts": print(sum(1 for x in d.get("phases", []) if x["name"] == a[0]))
|
|
83
|
+
elif op == "phase": # name status started ended exit commit pushed attempt — one entry per attempt, keyed by its number:
|
|
84
|
+
# a blocked attempt and its resume can start in the same second on a fast machine, and keying by the
|
|
85
|
+
# start made the second overwrite the first (dev_001 on the CI runner, 2026-10-09)
|
|
86
|
+
ph = d.get("phases", [])
|
|
87
|
+
new = dict(zip(("name", "status", "started", "ended", "goal_exit", "commit", "pushed", "attempt"), a))
|
|
88
|
+
hit = [i for i, x in enumerate(ph) if x["name"] == a[0] and str(x.get("attempt", "")) == a[7]]
|
|
89
|
+
if hit: ph[hit[0]] = new
|
|
90
|
+
else: ph.append(new)
|
|
91
|
+
d["phases"] = ph
|
|
92
|
+
if op in ("init", "phase"): json.dump(d, open(p, "w"), indent=1)
|
|
93
|
+
PY
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if [[ $REPORT_ONLY -eq 1 ]]; then :
|
|
97
|
+
elif [[ $RESUME -eq 1 ]]; then
|
|
98
|
+
[[ -f "$STATE" ]] || { echo "tf-develop: --resume: no $STATE" >&2; exit 2; }
|
|
99
|
+
APP="$(st get app)"; BRIEF="$(st get brief)"; HARNESS="$(st get harness)"; MODEL="$(st get model)"; TIER="$(st get tier)"
|
|
100
|
+
[[ "$(st get push)" == "False" ]] && PUSH=0
|
|
101
|
+
else
|
|
102
|
+
[[ -n "$APP" && -n "$BRIEF" ]] || { echo "tf-develop: --app <App> and --brief <file> are required" >&2; exit 2; }
|
|
103
|
+
[[ "$APP" =~ ^[A-Z][A-Za-z0-9]*$ ]] || { echo "tf-develop: --app must be PascalCase with no spaces" >&2; exit 2; }
|
|
104
|
+
[[ -f "$BRIEF" ]] || { echo "tf-develop: brief not found: $BRIEF" >&2; exit 2; }
|
|
105
|
+
own_repo || { echo "tf-develop: $DIR is not a git repository of its own (a folder inside another repository does not count) — set it up first" >&2; exit 2; }
|
|
106
|
+
if [[ $PUSH -eq 1 && -z "$(git -C "$DIR" remote 2>/dev/null)" ]]; then echo "tf-develop: $DIR has no git remote to push to — add one, or pass --no-push" >&2; exit 2; fi
|
|
107
|
+
if [[ $DRY -eq 0 ]]; then
|
|
108
|
+
[[ "$(cd "$(dirname "$BRIEF")" && pwd)/$(basename "$BRIEF")" == "$DIR/docs/$APP-Brief.md" ]] || cp "$BRIEF" "$DIR/docs/$APP-Brief.md"
|
|
109
|
+
st init "$APP" "docs/$APP-Brief.md" "$HARNESS" "$MODEL" "$TIER" "$PUSH" "$(ts)"
|
|
110
|
+
fi
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
MODE="Act as the flow-master (.tfcore/agents/flow-master.md). The command is *develop-end-to-end for $APP, in develop mode: read .tfcore/tasks/_develop-mode.md first and follow it in every task — no question to the owner, no stop for an owner review; every decision the owner would make is yours, written where that rule says. The brief is docs/$APP-Brief.md."
|
|
114
|
+
goal_of() {
|
|
115
|
+
case "$1" in
|
|
116
|
+
day1) echo "$MODE Run *day1-greenfield $APP, stage 1 (.tfcore/tasks/day1-greenfield.md), with the brief as the concept, all of it. When stage 1 is complete and rendered, the goal is met: do not stop for review and do not start stage 2." ;;
|
|
117
|
+
day1-2) echo "$MODE Run *day1-greenfield $APP --stage2. The goal is met when stage 2 is complete and its status gate has run." ;;
|
|
118
|
+
build) echo "$MODE Run *build-phase $APP (.tfcore/tasks/build-phase.md) on the whole phase checklist: every row built, the verifier chained inline, FIX mode on every failing row, until every row is terminal. Create and migrate the database the Architecture names, locally, as that rule says." ;;
|
|
119
|
+
handoff) echo "$MODE Run *handoff-phase $APP (.tfcore/tasks/handoff-phase.md): the UsageGuide with its smoke checklist and test users, the guides, the HTMLs, the status gate. The goal is met when the app is ready for the owner's UAT." ;;
|
|
120
|
+
esac
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
commit_phase() { # $1 phase → prints "<sha> <pushed yes|no|off>"
|
|
124
|
+
local sha="" pushed="off"
|
|
125
|
+
own_repo || { echo "none no"; return; } # never commit into a repository the app folder merely sits inside
|
|
126
|
+
if [[ -n "$(git -C "$DIR" status --porcelain 2>/dev/null)" ]]; then
|
|
127
|
+
git -C "$DIR" add -A >/dev/null 2>&1
|
|
128
|
+
git -C "$DIR" commit -q -m "TechieFlow develop-end-to-end: $1 done — $APP" -m "Written by tf-develop.sh after the $1 phase; per-phase time and tokens in docs/metrics/develop-report.json." >/dev/null 2>&1
|
|
129
|
+
fi
|
|
130
|
+
sha="$(git -C "$DIR" rev-parse --short HEAD 2>/dev/null)"
|
|
131
|
+
if [[ $PUSH -eq 1 ]]; then
|
|
132
|
+
if git -C "$DIR" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
|
|
133
|
+
git -C "$DIR" push -q >/dev/null 2>&1 && pushed=yes || pushed=no
|
|
134
|
+
else
|
|
135
|
+
git -C "$DIR" push -q -u "$(git -C "$DIR" remote | head -1)" HEAD >/dev/null 2>&1 && pushed=yes || pushed=no
|
|
136
|
+
fi
|
|
137
|
+
fi
|
|
138
|
+
echo "${sha:-none} $pushed"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
report() { # writes docs/metrics/develop-report.json and docs/<App>-Build-Report.md from the state and runs.jsonl
|
|
142
|
+
python3 - "$STATE" "$DIR" <<'PY'
|
|
143
|
+
import datetime, json, os, sys
|
|
144
|
+
state, root = json.load(open(sys.argv[1])), sys.argv[2]
|
|
145
|
+
app = state["app"]
|
|
146
|
+
def t(s):
|
|
147
|
+
try: return datetime.datetime.strptime(s, "%Y-%m-%dT%H:%M:%SZ")
|
|
148
|
+
except Exception: return None
|
|
149
|
+
runs = []
|
|
150
|
+
try:
|
|
151
|
+
for line in open(os.path.join(root, "docs", "metrics", "runs.jsonl"), encoding="utf-8", errors="replace"):
|
|
152
|
+
try: r = json.loads(line)
|
|
153
|
+
except Exception: continue
|
|
154
|
+
if r.get("kind", "run") == "run" and r.get("app") in (None, app) and r.get("started"): runs.append(r)
|
|
155
|
+
except OSError: pass
|
|
156
|
+
voided = {(r.get("cmd"), r.get("started")) for r in runs if r.get("kind") == "run-void"}
|
|
157
|
+
def hm(s):
|
|
158
|
+
s = int(s or 0); return f"{s // 3600} h {s % 3600 // 60:02d} min" if s >= 3600 else f"{s // 60} min {s % 60:02d} s"
|
|
159
|
+
NAMES = {"day1": "Day 1, stage 1 (BRD, Architecture, mockups)", "day1-2": "Day 1, stage 2 (checklist, documents)",
|
|
160
|
+
"build": "Build and verify", "handoff": "Handoff (UsageGuide, UAT-ready)"}
|
|
161
|
+
out, tot = [], {"wall_s": 0, "work_s": 0, "tokens_in": 0, "tokens_out": 0, "tokens_cache_read": 0, "tokens_cache_write": 0, "runs": 0}
|
|
162
|
+
models = {}
|
|
163
|
+
# One row per phase, summing every attempt of it (a phase stopped "blocked" and resumed is run again; its
|
|
164
|
+
# first attempts' time and tokens are part of what the phase cost). A run record belongs to the attempt
|
|
165
|
+
# whose window holds its start, the window's first second included and its last excluded: a record that
|
|
166
|
+
# starts the second the previous phase ends belongs to the next one (the first proof run counted the
|
|
167
|
+
# build's 301,201 tokens under Day 1 stage 2).
|
|
168
|
+
order, attempts = [], {}
|
|
169
|
+
for x in state.get("phases", []):
|
|
170
|
+
if x["name"] not in attempts: order.append(x["name"])
|
|
171
|
+
attempts.setdefault(x["name"], []).append(x)
|
|
172
|
+
def inside(r, wins):
|
|
173
|
+
s = t(r.get("started"))
|
|
174
|
+
return s is not None and any(a <= s < b for a, b in wins)
|
|
175
|
+
for name in order:
|
|
176
|
+
ats = attempts[name]; p = ats[-1]
|
|
177
|
+
wins = [(t(x.get("started")), t(x.get("ended")) or datetime.datetime.utcnow()) for x in ats if t(x.get("started"))]
|
|
178
|
+
rs = [r for r in runs if (r.get("cmd"), r.get("started")) not in voided and inside(r, wins)]
|
|
179
|
+
# A record's tokens are measured over its own window of the session, so a command chained inside
|
|
180
|
+
# another (devguide inside handoff-phase) is counted twice if both are summed: a record lying wholly
|
|
181
|
+
# inside another is dropped, and working time is the union of the windows, not their sum.
|
|
182
|
+
span = lambda r: (t(r.get("started")), t(r.get("ended")) or t(r.get("started")))
|
|
183
|
+
rs = [r for i, r in enumerate(rs) if not any(
|
|
184
|
+
j != i and span(o)[0] and span(r)[0] and span(o)[0] <= span(r)[0] and span(r)[1] <= span(o)[1]
|
|
185
|
+
and (span(o) != span(r) or j < i) for j, o in enumerate(rs))]
|
|
186
|
+
iv = sorted(span(r) for r in rs if span(r)[0]); union, cur = 0, None
|
|
187
|
+
for a_, b_ in iv:
|
|
188
|
+
if cur and a_ <= cur[1]: cur = (cur[0], max(cur[1], b_))
|
|
189
|
+
else:
|
|
190
|
+
if cur: union += (cur[1] - cur[0]).total_seconds()
|
|
191
|
+
cur = (a_, b_)
|
|
192
|
+
if cur: union += (cur[1] - cur[0]).total_seconds()
|
|
193
|
+
row = {"phase": name, "title": NAMES.get(name, name), "status": p.get("status"), "attempts": len(ats),
|
|
194
|
+
"started": ats[0].get("started"), "ended": p.get("ended"),
|
|
195
|
+
"wall_s": int(sum((b - a).total_seconds() for a, b in wins)), "work_s": int(union),
|
|
196
|
+
"tokens_in": sum(int(r.get("tokens_in") or 0) for r in rs), "tokens_out": sum(int(r.get("tokens_out") or 0) for r in rs),
|
|
197
|
+
"tokens_cache_read": sum(int(r.get("tokens_cache_read") or 0) for r in rs),
|
|
198
|
+
"tokens_cache_write": sum(int(r.get("tokens_cache_write") or 0) for r in rs),
|
|
199
|
+
"runs": len(rs), "commands": sorted({r.get("cmd") for r in rs if r.get("cmd")}), "commit": p.get("commit"), "pushed": p.get("pushed")}
|
|
200
|
+
for r in rs:
|
|
201
|
+
for m, n in (r.get("model_tokens_out") or {r.get("model") or "unknown": r.get("tokens_out") or 0}).items():
|
|
202
|
+
models[m] = models.get(m, 0) + int(n or 0)
|
|
203
|
+
for k in tot: tot[k] += row[k]
|
|
204
|
+
out.append(row)
|
|
205
|
+
done = [p for p in out if p["status"] == "done"]
|
|
206
|
+
ready = len(done) == 4
|
|
207
|
+
starts = [t(p["started"]) for p in out if t(p["started"])]; ends = [t(p["ended"]) for p in out if t(p["ended"])]
|
|
208
|
+
span = int((max(ends) - min(starts)).total_seconds()) if starts and ends else 0
|
|
209
|
+
rep = {"app": app, "harness": state.get("harness"), "model": state.get("model") or None, "tier": state.get("tier"),
|
|
210
|
+
"started": state.get("started"), "status": "uat-ready" if ready else (out[-1]["status"] if out else "not-started"),
|
|
211
|
+
"elapsed_s": span, "totals": tot, "tokens_out_by_model": models, "phases": out,
|
|
212
|
+
"note": "elapsed_s is first phase start to last phase end, usage-limit waits included; work_s is the agent's own run time from its run records"}
|
|
213
|
+
json.dump(rep, open(os.path.join(root, "docs", "metrics", "develop-report.json"), "w"), indent=1)
|
|
214
|
+
L = [f"# {app} — Build Report", "", "| | |", "|---|---|",
|
|
215
|
+
f"| What | {app}, developed end to end from docs/{app}-Brief.md by *develop-end-to-end |",
|
|
216
|
+
f"| Result | {'Ready for UAT' if ready else 'Stopped at ' + (out[-1]['title'] if out else 'the start') + ' (' + rep['status'] + ')'} |",
|
|
217
|
+
f"| Built in | {hm(span)} from start to finish, of which the agent worked {hm(tot['work_s'])} |",
|
|
218
|
+
f"| Harness and model | {state.get('harness')}, {', '.join(sorted(models)) or state.get('model') or 'default'} |",
|
|
219
|
+
f"| Tokens | {tot['tokens_in']:,} in, {tot['tokens_out']:,} out, {tot['tokens_cache_read']:,} read from cache, {tot['tokens_cache_write']:,} written to cache |",
|
|
220
|
+
"", "## Phases", "", "| Phase | Result | Time | Agent working | Tokens out | Commit |", "|---|---|---|---|---|---|"]
|
|
221
|
+
for p in out:
|
|
222
|
+
L.append(f"| {p['title']}{' (' + str(p['attempts']) + ' attempts)' if p['attempts'] > 1 else ''} | {p['status']} | {hm(p['wall_s'])} | {hm(p['work_s'])} | {p['tokens_out']:,} | {p['commit'] or '—'}{'' if p['pushed'] in ('yes', 'off') else ' (not pushed)'} |")
|
|
223
|
+
L += ["", "Time is the clock from the phase's start to its end, summed over its attempts, waits for a usage limit to reset included. "
|
|
224
|
+
"Built in also counts the time between attempts, while a stopped run waited to be resumed. "
|
|
225
|
+
"Agent working is the sum of the phase's run records. The same figures, for a comparison harness, are in "
|
|
226
|
+
"`docs/metrics/develop-report.json`.", ""]
|
|
227
|
+
open(os.path.join(root, "docs", f"{app}-Build-Report.md"), "w", encoding="utf-8").write("\n".join(L))
|
|
228
|
+
print(f"report: {rep['status']}, {hm(span)} elapsed, {tot['tokens_out']:,} tokens out — docs/{app}-Build-Report.md")
|
|
229
|
+
PY
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# No --model: the tier's routed model (.tfcore/routing.yaml), never the machine's own default. The proof
|
|
233
|
+
# run on OpenCode fell back to a local model server that was not running and failed every cycle.
|
|
234
|
+
if [[ -z "$MODEL" && -f "$DIR/.tfcore/utils/tf-model-pick.sh" ]]; then
|
|
235
|
+
MODEL="$(cd "$DIR" && bash .tfcore/utils/tf-model-pick.sh pick "$TIER" "$HARNESS" 2>/dev/null)"
|
|
236
|
+
[[ "$MODEL" == inherit ]] && MODEL=""
|
|
237
|
+
fi
|
|
238
|
+
# --report: write the report again from the saved state (the run's, or the finished run's), nothing else
|
|
239
|
+
if [[ $REPORT_ONLY -eq 1 ]]; then
|
|
240
|
+
[[ -f "$STATE" ]] || STATE="$DIR/.tfcore/.session/develop-done.json"
|
|
241
|
+
[[ -f "$STATE" ]] || { echo "tf-develop: --report: no develop run recorded here" >&2; exit 2; }
|
|
242
|
+
report; exit 0
|
|
243
|
+
fi
|
|
244
|
+
GOAL_ARGS=(--harness "$HARNESS" --tier "$TIER"); [[ -n "$MODEL" ]] && GOAL_ARGS+=(--model "$MODEL")
|
|
245
|
+
for phase in day1 day1-2 build handoff; do
|
|
246
|
+
if [[ $DRY -eq 1 ]]; then echo "$phase: bash $GOAL_SH ${GOAL_ARGS[*]} $DIR \"$(goal_of "$phase" | cut -c1-90)…\"; then commit$([[ $PUSH -eq 1 ]] && echo ' and push')"; continue; fi
|
|
247
|
+
[[ -n "$(st done "$phase")" ]] && { log "$phase already done — skipped"; continue; }
|
|
248
|
+
start="$(ts)"; log "$phase started"; attempt=$(( $(st attempts "$phase") + 1 ))
|
|
249
|
+
st phase "$phase" running "$start" "" "" "" "" "$attempt"
|
|
250
|
+
bash "$GOAL_SH" "${GOAL_ARGS[@]}" "$DIR" "$(goal_of "$phase")"; rc=$?
|
|
251
|
+
status=done; [[ $rc -eq 3 ]] && status=blocked; [[ $rc -ne 0 && $rc -ne 3 ]] && status="failed (exit $rc)"
|
|
252
|
+
end="$(ts)"
|
|
253
|
+
st phase "$phase" "$status" "$start" "$end" "$rc" "" "" "$attempt"
|
|
254
|
+
report >/dev/null # the phase's figures go into its own commit
|
|
255
|
+
read -r sha pushed <<<"$(commit_phase "$phase")"
|
|
256
|
+
st phase "$phase" "$status" "$start" "$end" "$rc" "$sha" "$pushed" "$attempt"
|
|
257
|
+
log "$phase $status — commit $sha, pushed $pushed"
|
|
258
|
+
if [[ $rc -ne 0 ]]; then report; commit_phase "report" >/dev/null; exit "$rc"; fi
|
|
259
|
+
done
|
|
260
|
+
[[ $DRY -eq 1 ]] && exit 0
|
|
261
|
+
report; read -r sha pushed <<<"$(commit_phase "build report")"
|
|
262
|
+
log "UAT-ready — final report commit $sha, pushed $pushed"
|
|
263
|
+
mv -f "$STATE" "$DIR/.tfcore/.session/develop-done.json" # develop mode ends with the run
|
|
264
|
+
exit 0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
bash .tfcore/utils/tf-devguide-list.sh <App> [--phase N] [--update]
|
|
5
5
|
|
|
6
|
-
Reads appKind and appPhase from .tfcore/core-config.yaml and prints:
|
|
6
|
+
Reads appKind (or metrics.project_type) and appPhase from .tfcore/core-config.yaml and prints:
|
|
7
7
|
app every routed page found in the code (route, file), matched to the phase's
|
|
8
8
|
UIDesign screens and to the roles in the UsageGuide's Test users table;
|
|
9
9
|
UIDesign screens with no code (a defect), routes with no UIDesign screen
|
|
@@ -50,9 +50,16 @@ def cfg(key, default):
|
|
|
50
50
|
PRUNE = {"bin", "obj", "node_modules", ".git", ".artifacts", "OldDocs", "dist", ".tfcore", ".claude", ".opencode", "packages", "TestResults"}
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
# The folders a project keeps its code in, in the order tf-verify-boot.sh looks (Lekhak TF-014: Lekhak's
|
|
54
|
+
# is source/, and --update counted only src/).
|
|
55
|
+
CODE_ROOTS = ("src", "source")
|
|
56
|
+
|
|
57
|
+
|
|
53
58
|
def walk(root, exts, skip_samples=False):
|
|
54
59
|
for d, dirs, files in os.walk(root):
|
|
55
|
-
|
|
60
|
+
# prune in place: never descend into build output, nor into any hidden folder (.tfbuild, .vs,
|
|
61
|
+
# .playwright …), which holds tool output, never a page (Lekhak TF-014)
|
|
62
|
+
dirs[:] = [x for x in dirs if x not in PRUNE and not x.startswith(".")]
|
|
56
63
|
dd = d.replace("\\", "/") + "/"
|
|
57
64
|
if any(s in dd for s in SKIP):
|
|
58
65
|
continue
|
|
@@ -60,20 +67,28 @@ def walk(root, exts, skip_samples=False):
|
|
|
60
67
|
continue
|
|
61
68
|
for f in files:
|
|
62
69
|
if f.lower().endswith(exts):
|
|
63
|
-
|
|
70
|
+
p = os.path.join(d, f).replace("\\", "/")
|
|
71
|
+
# only a leading "./" goes: lstrip("./") also ate the dot of a hidden folder, and
|
|
72
|
+
# .tfbuild/… became tfbuild/…, which does not exist (Lekhak TF-014)
|
|
73
|
+
yield p[2:] if p.startswith("./") else p
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def code_files(exts, skip_samples=True):
|
|
77
|
+
"""Every file with these extensions under the project's code folders (src/, source/)."""
|
|
78
|
+
for r in CODE_ROOTS:
|
|
79
|
+
if os.path.isdir(r):
|
|
80
|
+
yield from walk(r, exts, skip_samples=skip_samples)
|
|
64
81
|
|
|
65
82
|
|
|
66
83
|
def roles_from_usageguide(app):
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
roles.append((c[1].strip("`* "), c[3].strip("`* ")))
|
|
76
|
-
return roles
|
|
84
|
+
# the guide may be {App}-UsageGuide.md or {App}-Usage-Guide.md, its heading ## or ###, and its
|
|
85
|
+
# table the template's numbered shape or one read by its header names (AppManager TF-009):
|
|
86
|
+
# the same reader tf-verify-list.py uses
|
|
87
|
+
import importlib.util
|
|
88
|
+
spec = importlib.util.spec_from_file_location("tf_verify_list", os.path.join(os.path.dirname(os.path.abspath(__file__)), "tf-verify-list.py"))
|
|
89
|
+
vl = importlib.util.module_from_spec(spec)
|
|
90
|
+
spec.loader.exec_module(vl)
|
|
91
|
+
return [(u["user"], u["role"]) for u in vl.test_users(vl.usage_guide(app))]
|
|
77
92
|
|
|
78
93
|
|
|
79
94
|
def screens_from_uidesign(app, phase):
|
|
@@ -88,9 +103,9 @@ def screens_from_uidesign(app, phase):
|
|
|
88
103
|
|
|
89
104
|
def routes_in_code():
|
|
90
105
|
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
|
|
106
|
+
for f in walk(".", (".razor", ".cshtml", ".cs", ".ts", ".tsx", ".js", ".jsx", ".vue"), skip_samples=True):
|
|
107
|
+
if "/" not in f or re.search(r"\.(spec|test)\.\w+$", f):
|
|
108
|
+
continue # a top-level file is never a page; a test's `path:` is a screenshot, not a route (TfLens TF-050)
|
|
94
109
|
txt = read(f)
|
|
95
110
|
for ext_re, pat, what in ROUTE_PATTERNS:
|
|
96
111
|
if re.search(ext_re, f, re.I):
|
|
@@ -136,12 +151,16 @@ def main(argv):
|
|
|
136
151
|
app = argv[1]
|
|
137
152
|
update = "--update" in argv
|
|
138
153
|
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", "
|
|
154
|
+
kind = cfg("appKind", "").lower()
|
|
155
|
+
if not kind: # metrics.project_type: library says it when appKind is not set (TrBlazeUI TF-005)
|
|
156
|
+
pt = re.search(r"(?m)^\s+project_type:\s*(\S+)", read(os.path.join(".tfcore", "core-config.yaml"))) \
|
|
157
|
+
if os.path.isfile(os.path.join(".tfcore", "core-config.yaml")) else None
|
|
158
|
+
kind = pt.group(1).strip("'\"").lower() if pt else "app"
|
|
140
159
|
if "--kind" in argv and argv.index("--kind") + 1 < len(argv):
|
|
141
160
|
kind = argv[argv.index("--kind") + 1].lower() # override for a project whose config lacks it
|
|
142
161
|
sub = "app"
|
|
143
162
|
if kind.startswith("lib"):
|
|
144
|
-
razors =
|
|
163
|
+
razors = list(code_files((".razor",)))
|
|
145
164
|
sub = "ui-library" if razors else "service-library"
|
|
146
165
|
print(f"# tf-devguide-list — {app} — kind {sub} — phase {phase}")
|
|
147
166
|
print()
|
|
@@ -184,9 +203,9 @@ def main(argv):
|
|
|
184
203
|
for k in sorted(endpoints):
|
|
185
204
|
print(f"- {k} — {', '.join(sorted(set(endpoints[k]))[:2])}")
|
|
186
205
|
elif sub == "ui-library":
|
|
187
|
-
comps =
|
|
206
|
+
comps = list(code_files((".razor",)))
|
|
188
207
|
if not comps:
|
|
189
|
-
print("NOTHING: no component under src/. Build first.")
|
|
208
|
+
print("NOTHING: no component under src/ or source/. Build first.")
|
|
190
209
|
return 0
|
|
191
210
|
samples = sample_files((".razor", ".cshtml", ".html"))
|
|
192
211
|
stxt = {f: read(f) for f in samples}
|
|
@@ -196,9 +215,9 @@ def main(argv):
|
|
|
196
215
|
shown = [s for s, t in stxt.items() if re.search(rf"<{re.escape(name)}\b", t)]
|
|
197
216
|
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
217
|
else:
|
|
199
|
-
files =
|
|
218
|
+
files = list(code_files((".cs", ".ts", ".py", ".go", ".java")))
|
|
200
219
|
if not files:
|
|
201
|
-
print("NOTHING: no source under src/. Build first.")
|
|
220
|
+
print("NOTHING: no source under src/ or source/. Build first.")
|
|
202
221
|
return 0
|
|
203
222
|
items = []
|
|
204
223
|
for f in files:
|
|
@@ -222,6 +241,16 @@ def main(argv):
|
|
|
222
241
|
print()
|
|
223
242
|
if ver is None:
|
|
224
243
|
print(f"## --update: no existing guide at {gp} (or no \"Verified on\"); everything is new")
|
|
244
|
+
# a guide kept under another name or folder (Lekhak TF-014: docs/devguides/, one file per
|
|
245
|
+
# role) is named, so it is read and carried over, never silently rewritten from scratch
|
|
246
|
+
import glob
|
|
247
|
+
base = os.path.basename(gp)[:-len(".md")]
|
|
248
|
+
others = sorted(f for f in glob.glob(os.path.join("docs", "**", base + "*.md"), recursive=True)
|
|
249
|
+
if os.path.normpath(f) != os.path.normpath(gp))
|
|
250
|
+
if others:
|
|
251
|
+
print(f"- {len(others)} guide file(s) under another name: " + ", ".join(f.replace(os.sep, "/") for f in others[:8])
|
|
252
|
+
+ (" …" if len(others) > 8 else ""))
|
|
253
|
+
print(f" read them and carry every entry that still matches the code into {gp}; do not start from nothing")
|
|
225
254
|
else:
|
|
226
255
|
print(f"## --update: existing guide {gp}, Verified on {ver}, {len(entries)} entries")
|
|
227
256
|
try:
|
|
@@ -230,8 +259,8 @@ def main(argv):
|
|
|
230
259
|
except Exception:
|
|
231
260
|
cutoff = None
|
|
232
261
|
if cutoff:
|
|
233
|
-
changed = [f for f in
|
|
234
|
-
if
|
|
262
|
+
changed = [f for f in code_files((".razor", ".cshtml", ".cs", ".ts", ".tsx", ".js", ".py"), skip_samples=False)
|
|
263
|
+
if os.path.getmtime(f) > cutoff]
|
|
235
264
|
print(f"- {len(changed)} source file(s) changed since then; remap the entries they serve, keep the rest verbatim")
|
|
236
265
|
for f in changed[:30]:
|
|
237
266
|
print(f" - {f}")
|