@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
|
@@ -10,9 +10,14 @@
|
|
|
10
10
|
# Only when PROJECT-STATUS.md was written in this session (newer than the
|
|
11
11
|
# session pointer .tfcore/.session/<harness>.json):
|
|
12
12
|
# 2. tf-doc-check.sh FAILs on PROJECT-STATUS.md -> fix the file, re-run the check
|
|
13
|
+
# 2c. a BRD or UI design written this session leaves a screen with no row, UI design
|
|
14
|
+
# entry or mockup -> add it, re-run the check (TF-030)
|
|
13
15
|
# 3. docs/<App>-BRD.md older than docs/<App>-Checklist.md
|
|
14
16
|
# -> bash .tfcore/utils/tf-brd-status.sh <App>
|
|
15
17
|
# 4. no runs.jsonl record after the status write -> the run record (_metrics-emit-gate.md)
|
|
18
|
+
# Only on the turn that wrote PROJECT-STATUS.md (a command's closing turn):
|
|
19
|
+
# 5. tf-owner-text.sh FAILs on the closing message or a free-form document it
|
|
20
|
+
# hands the owner -> rewrite it for the owner
|
|
16
21
|
#
|
|
17
22
|
# Wired in .claude/settings.json -> hooks.Stop; OpenCode via
|
|
18
23
|
# .opencode/plugin/techieflow.js session.idle (a nudge: OpenCode has no blocking
|
|
@@ -28,6 +33,7 @@ import datetime
|
|
|
28
33
|
import glob
|
|
29
34
|
import json
|
|
30
35
|
import os
|
|
36
|
+
import re
|
|
31
37
|
import subprocess
|
|
32
38
|
import sys
|
|
33
39
|
|
|
@@ -58,10 +64,18 @@ elif md_mtime > os.path.getmtime(html):
|
|
|
58
64
|
def session_start():
|
|
59
65
|
"""mtime of the harness's session pointer; None when there is none (checks 2-4 skipped)."""
|
|
60
66
|
d = os.path.join(root, ".tfcore", ".session")
|
|
61
|
-
|
|
67
|
+
# The harness in the order tf-harness.sh and tf-emit.sh take it: TF_HARNESS (the OpenCode
|
|
68
|
+
# plugin sets it on every guard it spawns), then Claude Code's own variables, then OPENCODE*.
|
|
69
|
+
# OpenCode first let any OPENCODE_* in the owner's shell (an API key, a config path) send a
|
|
70
|
+
# Claude Code turn to opencode.json; with no such file, checks 2-5 never ran (2026-09-27).
|
|
71
|
+
tf = os.environ.get("TF_HARNESS")
|
|
72
|
+
if tf == "opencode":
|
|
62
73
|
cands = [os.path.join(d, "opencode.json")]
|
|
63
|
-
elif
|
|
74
|
+
elif tf == "claude-code" or any(os.environ.get(k) for k in (
|
|
75
|
+
"CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CLAUDE_CODE_SESSION_ID", "CLAUDE_PROJECT_DIR")):
|
|
64
76
|
cands = [os.path.join(d, "claude-code.json")]
|
|
77
|
+
elif any(k.startswith("OPENCODE") for k in os.environ):
|
|
78
|
+
cands = [os.path.join(d, "opencode.json")]
|
|
65
79
|
else:
|
|
66
80
|
cands = [os.path.join(d, "claude-code.json"), os.path.join(d, "opencode.json")]
|
|
67
81
|
times = [os.path.getmtime(p) for p in cands if os.path.isfile(p)]
|
|
@@ -104,6 +118,31 @@ if written_this_session:
|
|
|
104
118
|
except Exception:
|
|
105
119
|
pass
|
|
106
120
|
|
|
121
|
+
# 2c. a screen is not finished until it has its row, its UI design entry and its mockup. An
|
|
122
|
+
# amendment added a Price providers screen inside a requirement with none of the three, the
|
|
123
|
+
# checker's one finding was on the checklist and passed as old, and the page was built with no
|
|
124
|
+
# design (TfLens TF-030, 2026-09-11). On a turn that wrote a BRD or a UI design, those findings
|
|
125
|
+
# block; a BRD's other findings stay with *amend-docs, as before.
|
|
126
|
+
try:
|
|
127
|
+
chk = os.path.join(root, ".tfcore", "utils", "tf-doc-check.sh")
|
|
128
|
+
chain = re.compile(r'names the screen "|has no "### Screen:" entry|is in the UIDesign but not in the BRD'
|
|
129
|
+
r'|has no mockup link \(docs/mockups')
|
|
130
|
+
apps = set()
|
|
131
|
+
for f in glob.glob(os.path.join(root, "docs", "*.md")):
|
|
132
|
+
m = re.match(r"^(.+?)-(?:P\d+-)?(?:BRD|UIDesign)\.md$", os.path.basename(f))
|
|
133
|
+
if m and os.path.getmtime(f) >= start:
|
|
134
|
+
apps.add(m.group(1))
|
|
135
|
+
for app in sorted(apps) if os.path.isfile(chk) else ():
|
|
136
|
+
out = subprocess.run(["bash", chk, "--quiet", "--app", app], cwd=root,
|
|
137
|
+
capture_output=True, text=True, timeout=180).stdout
|
|
138
|
+
fails = [l for l in out.splitlines() if l.startswith("FAIL") and chain.search(l)]
|
|
139
|
+
if fails:
|
|
140
|
+
problems.append(f"a screen in {app}'s documents has no row, UI design entry or mockup ({len(fails)} line(s)). "
|
|
141
|
+
f"Add what is missing, then run: bash .tfcore/utils/tf-doc-check.sh --app {app}")
|
|
142
|
+
problems.extend(" " + l for l in fails[:5])
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
|
|
107
146
|
# 3. the BRD's Development status table is not older than the checklist
|
|
108
147
|
try:
|
|
109
148
|
for cl in glob.glob(os.path.join(root, "docs", "*-Checklist.md")):
|
|
@@ -136,6 +175,51 @@ if written_this_session:
|
|
|
136
175
|
except Exception:
|
|
137
176
|
pass
|
|
138
177
|
|
|
178
|
+
# 5. what the owner reads when a command ends: the closing message, and every free-form
|
|
179
|
+
# document it hands over that this turn wrote, through tf-owner-text.sh. TfLens's hand-off of
|
|
180
|
+
# 2026-09-11 reached the owner in jargon, named upstream problems with no word on what they
|
|
181
|
+
# touch, called two fixed ones open, and gave no prompt (MISS-TechieFlow-20260911-03). Only on
|
|
182
|
+
# the turn that closed a command -- PROJECT-STATUS written after this turn's prompt -- so an
|
|
183
|
+
# ordinary conversation is never held to a hand-off's shape. Claude Code hands the hook the
|
|
184
|
+
# message; the OpenCode plugin passes the same two fields.
|
|
185
|
+
try:
|
|
186
|
+
message = data.get("last_assistant_message") or ""
|
|
187
|
+
turn = data.get("turn_started")
|
|
188
|
+
if turn is None and data.get("transcript_path") and os.path.isfile(data["transcript_path"]):
|
|
189
|
+
with open(data["transcript_path"], encoding="utf-8", errors="replace") as f:
|
|
190
|
+
for line in f:
|
|
191
|
+
try:
|
|
192
|
+
r = json.loads(line)
|
|
193
|
+
except Exception:
|
|
194
|
+
continue
|
|
195
|
+
c = (r.get("message") or {}).get("content")
|
|
196
|
+
prompt = r.get("type") == "user" and not r.get("isMeta") and (
|
|
197
|
+
isinstance(c, str) or (isinstance(c, list) and any(isinstance(x, dict) and x.get("type") == "text" for x in c)))
|
|
198
|
+
if prompt and r.get("timestamp"):
|
|
199
|
+
turn = r["timestamp"]
|
|
200
|
+
if isinstance(turn, str):
|
|
201
|
+
turn = datetime.datetime.strptime(turn[:19], "%Y-%m-%dT%H:%M:%S").replace(
|
|
202
|
+
tzinfo=datetime.timezone.utc).timestamp()
|
|
203
|
+
elif isinstance(turn, (int, float)) and turn > 1e12:
|
|
204
|
+
turn = turn / 1000.0
|
|
205
|
+
chk = os.path.join(root, ".tfcore", "utils", "tf-owner-text.sh")
|
|
206
|
+
if message.strip() and turn and md_mtime >= turn - 2 and os.path.isfile(chk):
|
|
207
|
+
handed = []
|
|
208
|
+
for m in set(re.findall(r"(?<![\w/.-])((?:docs/)?[\w.-]+\.md)\b", message)):
|
|
209
|
+
p = os.path.join(root, m if m.startswith("docs/") else os.path.join("docs", m))
|
|
210
|
+
if os.path.isfile(p) and os.path.getmtime(p) >= turn - 2:
|
|
211
|
+
handed.append(os.path.relpath(p, root))
|
|
212
|
+
res = subprocess.run(["bash", chk, "--root", root, "--final", "--stdin"] + sorted(handed), cwd=root,
|
|
213
|
+
input=message, capture_output=True, text=True, timeout=60)
|
|
214
|
+
fails = [l for l in res.stdout.splitlines() if l.startswith("FAIL")]
|
|
215
|
+
if fails:
|
|
216
|
+
problems.append(f"what you are handing the owner is not written for the owner ({len(fails)} line(s)). "
|
|
217
|
+
"Fix each, then write the closing message again — plain words, every upstream problem "
|
|
218
|
+
"with what it affects and its prompt, and the next prompt in a code block:")
|
|
219
|
+
problems.extend(" " + l for l in fails[:8])
|
|
220
|
+
except Exception:
|
|
221
|
+
pass
|
|
222
|
+
|
|
139
223
|
if not problems:
|
|
140
224
|
sys.exit(0)
|
|
141
225
|
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
# - any `|| docker compose up` fallback, which widens the command precisely when it failed
|
|
27
27
|
# - `docker compose run|create`, `docker run`, `docker volume create` — creating, not starting
|
|
28
28
|
# - an inline or exported connection-string variable in front of a test/run command
|
|
29
|
-
# ALLOWS:
|
|
29
|
+
# ALLOWS: during *develop-end-to-end only, one `docker run|create --name <app>-db …` (_develop-mode.md);
|
|
30
|
+
# always: `docker start <name>`, `docker compose up <service>`, `docker compose start`,
|
|
30
31
|
# `docker ps|logs|inspect`, and anything that is not one of the above.
|
|
31
32
|
#
|
|
32
33
|
# Wired in .claude/settings.json -> hooks.PreToolUse (matcher "Bash"); OpenCode via
|
|
@@ -97,6 +98,20 @@ if re.search(r"\|\|\s*" + COMPOSE + r"\s+up\b", cmd, re.I):
|
|
|
97
98
|
"Drop the fallback. If the named service does not exist, the name is wrong — fix "
|
|
98
99
|
"the name or ask; do not start everything instead.")
|
|
99
100
|
|
|
101
|
+
# *develop-end-to-end (_develop-mode.md): the owner asked for the database to be created when none is
|
|
102
|
+
# reachable. While that run is on (.tfcore/.session/develop.json), ONE command is allowed through: a single
|
|
103
|
+
# `docker run|create` of the container that rule names, `--name <app>-db`, nothing else in the command.
|
|
104
|
+
# Found by the first proof run, which stopped "blocked" because this hook refused it (2026-10-08).
|
|
105
|
+
root = os.environ.get("CLAUDE_PROJECT_DIR") or os.environ.get("TF_PROJECT_DIR") or os.getcwd()
|
|
106
|
+
dev = os.path.join(root, ".tfcore", ".session", "develop.json")
|
|
107
|
+
if os.path.isfile(dev):
|
|
108
|
+
try: app = (json.load(open(dev)).get("app") or "").lower()
|
|
109
|
+
except Exception: app = ""
|
|
110
|
+
one = not re.search(r"(?:&&|\|\||;|\||\n|\$\(|`)", cmd) # && first: the portability check reads a pipe-ampersand pair as bash 4
|
|
111
|
+
if app and one and re.match(r"\s*docker\s+(?:run|create)\b", cmd, re.I) \
|
|
112
|
+
and re.search(r"--name[ =]%s-db(?:\s|$)" % re.escape(app), cmd):
|
|
113
|
+
sys.exit(0)
|
|
114
|
+
|
|
100
115
|
if re.search(COMPOSE + r"\s+(?:run|create)\b", cmd, re.I) or \
|
|
101
116
|
re.search(r"\bdocker\s+(?:run|create)\b", cmd, re.I) or \
|
|
102
117
|
re.search(r"\bdocker\s+volume\s+create\b", cmd, re.I):
|
|
@@ -99,14 +99,19 @@ if ledger is not None:
|
|
|
99
99
|
sys.exit(0) # a same-day ledger from a framework version before Sitting 4c: date unlocks
|
|
100
100
|
else:
|
|
101
101
|
rows = ledger.get("rows") or {}
|
|
102
|
-
|
|
102
|
+
# a ledger kept row by row holds older runs' rows too (Lekhak TF-007): only a row graded
|
|
103
|
+
# today unlocks
|
|
104
|
+
dates = ledger.get("row_dates")
|
|
105
|
+
not_listed = sorted(i for i in introduced if rows.get(i) != "PASS"
|
|
106
|
+
or (isinstance(dates, dict) and str(dates.get(i, "")) != today))
|
|
103
107
|
if not introduced:
|
|
104
108
|
# a Verified cell without a readable row id (a malformed line): refuse, nothing can vouch for it
|
|
105
109
|
not_listed = ["(row id not readable)"]
|
|
106
110
|
if not not_listed:
|
|
107
111
|
sys.exit(0)
|
|
108
|
-
problem = ("lists these rows as " + ", ".join(
|
|
109
|
-
|
|
112
|
+
problem = ("lists these rows as " + ", ".join(
|
|
113
|
+
f"{i}: {rows.get(i, 'absent')}" + (f" (graded {dates.get(i)})" if isinstance(dates, dict) and dates.get(i) and dates.get(i) != today else "")
|
|
114
|
+
for i in not_listed) + ", not PASS today")
|
|
110
115
|
|
|
111
116
|
print(
|
|
112
117
|
"BLOCKED by TechieFlow policy: this write INTRODUCES a `Verified` status, "
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
# test-results/ test-results-*/ scripts-*/ playwright-report/
|
|
27
27
|
# The project's own tracked scripts/ is never touched (needs the hyphen).
|
|
28
28
|
# tests/verify/ (promoted, tracked specs) is never touched (outside the roots).
|
|
29
|
+
# A file under the roots that a test names by path (tests/.artifacts/<folder>/<name>) is kept at
|
|
30
|
+
# any age, with everything below it (Lekhak TF-004); the summary says how many were kept.
|
|
29
31
|
#
|
|
30
32
|
# RETENTION: default 7 days. Override per project with `artifactRetentionDays: N`
|
|
31
33
|
# in .tfcore/core-config.yaml, or per run with TF_ARTIFACT_RETENTION_DAYS=N.
|
|
@@ -98,6 +100,42 @@ DAYS = retention_days()
|
|
|
98
100
|
CUTOFF = time.time() - DAYS * 86400
|
|
99
101
|
|
|
100
102
|
SWEEP_ROOTS = [os.path.join("tests", ".artifacts"), ".verify"]
|
|
103
|
+
|
|
104
|
+
# ---- what a test names is not litter (Lekhak TF-004) ---------------------------
|
|
105
|
+
# A spec that reads tests/.artifacts/harness/hindi-src lost it on the eighth day, and two rows failed
|
|
106
|
+
# "the crawl produced no result" on a working product. Any path under the sweep roots that a file of
|
|
107
|
+
# the project's tests (or its Playwright config) names is kept, with everything below it, whatever its
|
|
108
|
+
# age. The right home for a fixture is still tests/verify/fixtures/, outside the sweep.
|
|
109
|
+
# Only a NAMED thing inside a folder counts (tests/.artifacts/<folder>/<name>): a folder a test writes
|
|
110
|
+
# into is named with a variable after it (`shots/${name}.png`), which the pattern stops at, and the
|
|
111
|
+
# framework's own output folders are never kept. The Playwright config is not read: it names outputs.
|
|
112
|
+
KEEP_RE = re.compile(r"(?:tests[/\\]+\.artifacts|\.verify)[/\\]+[^'\"`\s)\]},;*?<>|${}+]+")
|
|
113
|
+
OUTPUTS = ("tests/.artifacts/verify/", "tests/.artifacts/build/", "tests/.artifacts/test-results",
|
|
114
|
+
"tests/.artifacts/playwright", "tests/.artifacts/regression", "tests/.artifacts/doc-check/")
|
|
115
|
+
TEXT_EXT = (".ts", ".js", ".mjs", ".cjs", ".cs", ".py", ".json", ".sh", ".cmd", ".ps1", ".yaml", ".yml", ".txt", ".md")
|
|
116
|
+
keep = set()
|
|
117
|
+
def scan_refs(fp):
|
|
118
|
+
try:
|
|
119
|
+
if os.path.getsize(fp) > 2 * 1024 * 1024:
|
|
120
|
+
return
|
|
121
|
+
with open(fp, encoding="utf-8", errors="replace") as fh:
|
|
122
|
+
for m in KEEP_RE.finditer(fh.read()):
|
|
123
|
+
k = re.sub(r"/+", "/", m.group(0).replace("\\", "/")).rstrip("/.")
|
|
124
|
+
if k.count("/") >= (2 if k.startswith(".verify/") else 3) and not (k + "/").startswith(OUTPUTS):
|
|
125
|
+
keep.add(k)
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
tests_dir = os.path.join(root, "tests")
|
|
129
|
+
if os.path.isdir(tests_dir) and not os.path.islink(tests_dir):
|
|
130
|
+
for dp, dns, fns in os.walk(tests_dir, followlinks=False):
|
|
131
|
+
dns[:] = [d for d in dns if d not in (".artifacts", "node_modules", "bin", "obj", ".git")]
|
|
132
|
+
for f in fns:
|
|
133
|
+
if f.endswith(TEXT_EXT):
|
|
134
|
+
scan_refs(os.path.join(dp, f))
|
|
135
|
+
kept_n = 0
|
|
136
|
+
def kept(fp):
|
|
137
|
+
rel = os.path.relpath(fp, root).replace(os.sep, "/")
|
|
138
|
+
return any(rel == k or rel.startswith(k + "/") for k in keep)
|
|
101
139
|
LEGACY_ROOT_GLOBS = ["test-results", "test-results-*", "scripts-*", "playwright-report"]
|
|
102
140
|
|
|
103
141
|
removed_files = 0
|
|
@@ -162,6 +200,9 @@ if DAYS > 0:
|
|
|
162
200
|
except Exception:
|
|
163
201
|
continue
|
|
164
202
|
if st.st_mtime < CUTOFF:
|
|
203
|
+
if keep and kept(fp):
|
|
204
|
+
kept_n += 1
|
|
205
|
+
continue
|
|
165
206
|
rm_file(fp)
|
|
166
207
|
# remove dirs emptied by this sweep (never the root itself)
|
|
167
208
|
if dp != top:
|
|
@@ -193,6 +234,11 @@ if removed_files or removed_dirs or legacy:
|
|
|
193
234
|
f"tests/.artifacts/ and .verify/"]
|
|
194
235
|
if legacy:
|
|
195
236
|
parts.append("banned repo-root legacy dirs removed: " + ", ".join(sorted(legacy)))
|
|
237
|
+
if kept_n:
|
|
238
|
+
parts.append(f"kept {kept_n} old file(s) a test names by path (move them to tests/verify/fixtures/)")
|
|
196
239
|
print("; ".join(parts))
|
|
240
|
+
elif kept_n:
|
|
241
|
+
print(f"TechieFlow sweep-artifacts: kept {kept_n} file(s) older than {DAYS}d because a test names them by path; "
|
|
242
|
+
"move them to tests/verify/fixtures/")
|
|
197
243
|
PY
|
|
198
244
|
exit 0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# TechieFlow PreToolUse hook — while YOLO is on, nothing asks (owner, 2026-10-14: "When I am running
|
|
3
|
+
# something in YOLO mode I am expecting that it doesn't ask me for permission whatever the case is").
|
|
4
|
+
#
|
|
5
|
+
# While the flag is live (TF_YOLO=1, or .tfcore/.session/yolo.json not expired — tf-yolo.sh is-on
|
|
6
|
+
# decides) it answers every tool call with permissionDecision "allow", in the session already
|
|
7
|
+
# running. A refusal still wins: the other PreToolUse hooks (block-git.sh for version-control
|
|
8
|
+
# writes, guard-build.sh, guard-db.sh …) exit 2 and the call is blocked, and the deny rules in
|
|
9
|
+
# settings.json still apply. Claude Code still asks where a hook's allow is not enough (an ask rule,
|
|
10
|
+
# its own safety checks); only a session started with `claude --permission-mode bypassPermissions`
|
|
11
|
+
# drops those, and project settings cannot set that mode (Claude Code refuses it there).
|
|
12
|
+
# While YOLO is off it says nothing, and puts the OpenCode config back if a flag expired or went
|
|
13
|
+
# away without `off` (tf-yolo.sh sync).
|
|
14
|
+
#
|
|
15
|
+
# Wired in .claude/settings.json → hooks.PreToolUse (matcher "*"). OpenCode's side is the plugin's
|
|
16
|
+
# permission.ask and the OpenCode config tf-yolo.sh rewrites. Fails OPEN: no output, exit 0.
|
|
17
|
+
|
|
18
|
+
cat >/dev/null
|
|
19
|
+
ROOT="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
|
20
|
+
YOLO="$ROOT/.tfcore/utils/tf-yolo.sh"
|
|
21
|
+
[[ -f "$YOLO" ]] || exit 0
|
|
22
|
+
if CLAUDE_PROJECT_DIR="$ROOT" bash "$YOLO" is-on >/dev/null 2>&1; then
|
|
23
|
+
printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"TechieFlow YOLO is on: no permission prompts (tf-yolo.sh off to end it)"}}'
|
|
24
|
+
elif [[ -f "$ROOT/.tfcore/.session/yolo-perms.json" ]]; then
|
|
25
|
+
CLAUDE_PROJECT_DIR="$ROOT" bash "$YOLO" sync >/dev/null 2>&1
|
|
26
|
+
fi
|
|
27
|
+
exit 0
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// blazor-zoom.cs.txt — copy to the MAUI project's root as BlazorZoom.cs and set the namespace
|
|
2
|
+
// (coding-standards-dotnet.md §9b). Kept as .txt so a project whose .csproj sits at the repository root
|
|
3
|
+
// does not compile the framework's copy. Then two hooks:
|
|
4
|
+
// MainPage (the page holding the BlazorWebView):
|
|
5
|
+
// blazorWebView.BlazorWebViewInitialized += (sender, args) => BlazorZoom.Attach(args.WebView);
|
|
6
|
+
// Platforms/MacCatalyst/AppDelegate.cs (inside the MauiUIApplicationDelegate class):
|
|
7
|
+
// public override void BuildMenu(UIKit.IUIMenuBuilder builder) { base.BuildMenu(builder); BlazorZoom.BuildMenu(builder); }
|
|
8
|
+
// [Export("tfZoomIn:")] public void ZoomIn(NSObject aSender) => BlazorZoom.ZoomBy(1);
|
|
9
|
+
// [Export("tfZoomOut:")] public void ZoomOut(NSObject aSender) => BlazorZoom.ZoomBy(-1);
|
|
10
|
+
// [Export("tfZoomReset:")] public void ZoomReset(NSObject aSender) => BlazorZoom.Reset();
|
|
11
|
+
// Optional: set BlazorZoom.FirstLevel before the page is built (for example from the screen size) to open
|
|
12
|
+
// at more than 100% before anyone has zoomed; Cmd/Ctrl+0 still means 100%.
|
|
13
|
+
// Proved on Lekhak's BlogAdmin (macOS 27, 2026-10-11): bash .tfcore/utils/tf-verify-zoom.sh checks it.
|
|
14
|
+
|
|
15
|
+
#nullable enable
|
|
16
|
+
#if MACCATALYST
|
|
17
|
+
using Foundation;
|
|
18
|
+
using UIKit;
|
|
19
|
+
using WebKit;
|
|
20
|
+
#endif
|
|
21
|
+
|
|
22
|
+
namespace YourApp;
|
|
23
|
+
|
|
24
|
+
/// <summary>
|
|
25
|
+
/// Zoom in, zoom out and actual size for the BlazorWebView, with the operating system's keys.
|
|
26
|
+
/// </summary>
|
|
27
|
+
/// <remarks>
|
|
28
|
+
/// <para><b>Purpose:</b> [coding-standards-dotnet.md §9b] On a high-resolution Mac the text was too small and
|
|
29
|
+
/// Cmd+= did nothing: a Mac Catalyst WKWebView has no zoom of its own. Every Blazor Hybrid head zooms from 50% to
|
|
30
|
+
/// 300% in 10% steps, and the level is kept in MAUI Preferences across restarts.</para>
|
|
31
|
+
/// <para><b>Code Flow:</b> <c>MainPage</c> hands the platform web view to <see cref="Attach"/> once the BlazorWebView is
|
|
32
|
+
/// initialised, which applies the saved level. Mac Catalyst: the View menu's Zoom In (Cmd+=), Zoom Out (Cmd+-) and
|
|
33
|
+
/// Actual Size (Cmd+0), added by <see cref="BuildMenu"/> from the app delegate, call <see cref="ZoomBy"/> and
|
|
34
|
+
/// <see cref="Reset"/>, which set <c>WKWebView.PageZoom</c> on every attached web view, one per window. Windows: WebView2's own zoom (25% to 500%, uneven steps,
|
|
35
|
+
/// not kept) is turned off, and a script run on every page handles Ctrl+plus, Ctrl+minus, Ctrl+0 and Ctrl+wheel with
|
|
36
|
+
/// the same steps, posting each new level back to be saved (and keeping it for the session, so a page reload keeps
|
|
37
|
+
/// it). The WinUI WebView2 control does not expose WebView2's own ZoomFactor, so that cannot be saved instead.</para>
|
|
38
|
+
/// </remarks>
|
|
39
|
+
public static class BlazorZoom
|
|
40
|
+
{
|
|
41
|
+
/// <summary>The Preferences key the zoom level is kept under.</summary>
|
|
42
|
+
public const string PreferenceKey = "tf.zoom";
|
|
43
|
+
|
|
44
|
+
/// <summary>Smallest zoom, 50%.</summary>
|
|
45
|
+
public const double Min = 0.5;
|
|
46
|
+
|
|
47
|
+
/// <summary>Largest zoom, 300%.</summary>
|
|
48
|
+
public const double Max = 3.0;
|
|
49
|
+
|
|
50
|
+
/// <summary>One step, 10%.</summary>
|
|
51
|
+
public const double Step = 0.1;
|
|
52
|
+
|
|
53
|
+
/// <summary>The level used until one is saved; 1.0 unless the app sets it before the page is built.</summary>
|
|
54
|
+
public static double FirstLevel { get; set; } = 1.0;
|
|
55
|
+
|
|
56
|
+
/// <summary>The zoom level now in use, 1.0 being actual size.</summary>
|
|
57
|
+
public static double Level { get; private set; } = 1.0;
|
|
58
|
+
|
|
59
|
+
/// <summary>Rounds to a whole step and keeps the level inside 50% to 300%.</summary>
|
|
60
|
+
/// <param name="aLevel">The level asked for.</param>
|
|
61
|
+
/// <returns>The level that will be used.</returns>
|
|
62
|
+
public static double Clamp(double aLevel) => Math.Round(Math.Clamp(aLevel, Min, Max) * 10) / 10;
|
|
63
|
+
|
|
64
|
+
#if MACCATALYST
|
|
65
|
+
// every window's web view, weakly, so a Mac app with several BlazorWebView windows zooms them all (Chatur TF-010)
|
|
66
|
+
private static readonly List<WeakReference<WKWebView>> lstWebViews = new();
|
|
67
|
+
private static bool blnMenuSet;
|
|
68
|
+
|
|
69
|
+
/// <summary>Takes the platform web view and applies the saved level.</summary>
|
|
70
|
+
/// <param name="aPlatformWebView">The <c>BlazorWebViewInitializedEventArgs.WebView</c>.</param>
|
|
71
|
+
public static void Attach(object aPlatformWebView)
|
|
72
|
+
{
|
|
73
|
+
if (aPlatformWebView is WKWebView objWebView)
|
|
74
|
+
{
|
|
75
|
+
lstWebViews.Add(new WeakReference<WKWebView>(objWebView));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Apply(Preferences.Default.Get(PreferenceKey, FirstLevel));
|
|
79
|
+
if (!blnMenuSet && OperatingSystem.IsMacCatalystVersionAtLeast(26))
|
|
80
|
+
{
|
|
81
|
+
// From macOS 26 the main menu is built here: what an app delegate's BuildMenu changes is ignored.
|
|
82
|
+
// Format is off because its Font › Bigger and Smaller already own Cmd+= and Cmd+-, and UIKit
|
|
83
|
+
// silently drops a menu whose shortcut repeats one; Inspector is off because the app has none.
|
|
84
|
+
blnMenuSet = true;
|
|
85
|
+
var objConfig = new UIMainMenuSystemConfiguration
|
|
86
|
+
{
|
|
87
|
+
TextFormattingPreference = UIMenuSystemElementGroupPreference.Removed,
|
|
88
|
+
InspectorPreference = UIMenuSystemElementGroupPreference.Removed,
|
|
89
|
+
};
|
|
90
|
+
UIMainMenuSystem.SharedSystem.SetBuildConfiguration(objConfig, AddCommands);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// <summary>Adds Zoom In, Zoom Out and Actual Size to the top of the View menu, before macOS 26.</summary>
|
|
95
|
+
/// <param name="aBuilder">The builder the app delegate's <c>BuildMenu</c> receives.</param>
|
|
96
|
+
/// <remarks>The selectors are answered by <c>[Export]</c> methods on the app delegate, which is on the
|
|
97
|
+
/// responder chain whatever has focus inside the web view. From macOS 26 <see cref="Attach"/> does this.</remarks>
|
|
98
|
+
public static void BuildMenu(IUIMenuBuilder aBuilder)
|
|
99
|
+
{
|
|
100
|
+
if (!OperatingSystem.IsMacCatalystVersionAtLeast(26) && aBuilder.System?.Handle == UIMenuSystem.MainSystem.Handle)
|
|
101
|
+
{
|
|
102
|
+
aBuilder.RemoveMenu(UIMenuIdentifier.Format.GetConstant()!);
|
|
103
|
+
AddCommands(aBuilder);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// <summary>Zooms in (a positive step) or out (a negative one).</summary>
|
|
108
|
+
/// <param name="aSteps">How many 10% steps.</param>
|
|
109
|
+
public static void ZoomBy(int aSteps) => Apply(Level + (aSteps * Step));
|
|
110
|
+
|
|
111
|
+
/// <summary>Back to actual size.</summary>
|
|
112
|
+
public static void Reset() => Apply(1.0);
|
|
113
|
+
|
|
114
|
+
private static void AddCommands(IUIMenuBuilder aBuilder)
|
|
115
|
+
{
|
|
116
|
+
var objView = aBuilder.GetMenu(UIMenuIdentifier.View.GetConstant()!);
|
|
117
|
+
if (objView == null || objView.Children.Any(c => c.Title == "Zoom In"))
|
|
118
|
+
{
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
var objZoom = UIMenu.Create(string.Empty, null, UIMenuIdentifier.None, UIMenuOptions.DisplayInline, new UIMenuElement[]
|
|
123
|
+
{
|
|
124
|
+
UIKeyCommand.Create("Zoom In", null, new ObjCRuntime.Selector("tfZoomIn:"), "=", UIKeyModifierFlags.Command, null),
|
|
125
|
+
UIKeyCommand.Create("Zoom Out", null, new ObjCRuntime.Selector("tfZoomOut:"), "-", UIKeyModifierFlags.Command, null),
|
|
126
|
+
UIKeyCommand.Create("Actual Size", null, new ObjCRuntime.Selector("tfZoomReset:"), "0", UIKeyModifierFlags.Command, null),
|
|
127
|
+
});
|
|
128
|
+
aBuilder.ReplaceMenu(UIMenuIdentifier.View.GetConstant()!, objView.GetMenuByReplacingChildren(new UIMenuElement[] { objZoom }.Concat(objView.Children).ToArray()));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private static void Apply(double aLevel)
|
|
132
|
+
{
|
|
133
|
+
Level = Clamp(aLevel);
|
|
134
|
+
Preferences.Default.Set(PreferenceKey, Level);
|
|
135
|
+
lstWebViews.RemoveAll(w => !w.TryGetTarget(out _));
|
|
136
|
+
foreach (var objRef in lstWebViews)
|
|
137
|
+
{
|
|
138
|
+
if (objRef.TryGetTarget(out var objWebView))
|
|
139
|
+
{
|
|
140
|
+
objWebView.PageZoom = (nfloat)Level;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
#elif WINDOWS
|
|
145
|
+
private const string MessagePrefix = "tfzoom:";
|
|
146
|
+
|
|
147
|
+
/// <summary>Takes the platform web view, turns WebView2's own zoom off and installs the key and wheel handler.</summary>
|
|
148
|
+
/// <param name="aPlatformWebView">The <c>BlazorWebViewInitializedEventArgs.WebView</c>.</param>
|
|
149
|
+
public static async void Attach(object aPlatformWebView)
|
|
150
|
+
{
|
|
151
|
+
if (aPlatformWebView is not Microsoft.UI.Xaml.Controls.WebView2 objWebView || objWebView.CoreWebView2 == null)
|
|
152
|
+
{
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
var objCore = objWebView.CoreWebView2;
|
|
157
|
+
Level = Clamp(Preferences.Default.Get(PreferenceKey, FirstLevel));
|
|
158
|
+
objCore.Settings.IsZoomControlEnabled = false;
|
|
159
|
+
objCore.WebMessageReceived += (sender, args) =>
|
|
160
|
+
{
|
|
161
|
+
var strMessage = args.TryGetWebMessageAsString();
|
|
162
|
+
if (strMessage != null && strMessage.StartsWith(MessagePrefix, StringComparison.Ordinal)
|
|
163
|
+
&& double.TryParse(strMessage[MessagePrefix.Length..], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var dblLevel))
|
|
164
|
+
{
|
|
165
|
+
Level = Clamp(dblLevel);
|
|
166
|
+
Preferences.Default.Set(PreferenceKey, Level);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var strLevel = Level.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
|
170
|
+
await objCore.AddScriptToExecuteOnDocumentCreatedAsync(
|
|
171
|
+
"(function(){var z=parseFloat(sessionStorage.getItem('tfzoom'))||" + strLevel + ",min=0.5,max=3;"
|
|
172
|
+
+ "function set(v){z=Math.round(Math.min(max,Math.max(min,v))*10)/10;document.documentElement.style.zoom=z;"
|
|
173
|
+
+ "sessionStorage.setItem('tfzoom',z);window.chrome.webview.postMessage('" + MessagePrefix + "'+z);}"
|
|
174
|
+
+ "function apply(){document.documentElement.style.zoom=z;}"
|
|
175
|
+
+ "if(document.documentElement){apply();}document.addEventListener('DOMContentLoaded',apply);"
|
|
176
|
+
+ "window.addEventListener('keydown',function(e){if(!e.ctrlKey||e.altKey||e.metaKey)return;"
|
|
177
|
+
+ "if(e.key==='='||e.key==='+'){set(z+0.1);}else if(e.key==='-'){set(z-0.1);}else if(e.key==='0'){set(1);}else return;"
|
|
178
|
+
+ "e.preventDefault();},true);"
|
|
179
|
+
+ "window.addEventListener('wheel',function(e){if(!e.ctrlKey)return;e.preventDefault();set(z+(e.deltaY<0?0.1:-0.1));},{passive:false});"
|
|
180
|
+
+ "})();");
|
|
181
|
+
// the page already loaded before the script was added: give it the level too
|
|
182
|
+
await objCore.ExecuteScriptAsync("document.documentElement.style.zoom=" + strLevel + ";");
|
|
183
|
+
}
|
|
184
|
+
#else
|
|
185
|
+
/// <summary>No zoom on this head.</summary>
|
|
186
|
+
/// <param name="aPlatformWebView">Unused.</param>
|
|
187
|
+
public static void Attach(object aPlatformWebView)
|
|
188
|
+
{
|
|
189
|
+
}
|
|
190
|
+
#endif
|
|
191
|
+
}
|
|
@@ -69,13 +69,33 @@ Controller actions: the `a` prefix applies to `[FromRoute]`, `[FromQuery]` and `
|
|
|
69
69
|
## 7. Testability
|
|
70
70
|
|
|
71
71
|
- Blazor: a stable `data-testid` (or element id) on every control the verifier must reach.
|
|
72
|
-
- MAUI: a stable, unique `AutomationId` on every interactive or data-bound control, named by intent (`LoginSubmitButton`, `ClientsGrid`), set on the element whose data the gate asserts.
|
|
72
|
+
- MAUI: a stable, unique `AutomationId` on every interactive or data-bound control, named by intent (`LoginSubmitButton`, `ClientsGrid`), set on the element whose data the gate asserts. The tab, menu item or button that opens a screen carries `AutomationId="nav-<screen>"` (the screen's name, lower case, words joined by `-`), or a label equal to the screen's name; the Mac check reaches screens that way.
|
|
73
73
|
|
|
74
74
|
## 8. Security
|
|
75
75
|
|
|
76
76
|
- No credentials in code. Parameterised queries. Validate inputs. Log security events.
|
|
77
77
|
|
|
78
|
-
## 9.
|
|
78
|
+
## 9. Mac Catalyst heads
|
|
79
|
+
|
|
80
|
+
Every MAUI project with a `net*-maccatalyst` target, Blazor Hybrid included:
|
|
81
|
+
|
|
82
|
+
- **Scene manifest.** `Platforms/MacCatalyst/Info.plist` carries `UIApplicationSceneManifest` with a `UIWindowSceneSessionRoleApplication` configuration named `__MAUI_DEFAULT_SCENE_CONFIGURATION__` whose `UISceneDelegateClassName` is `SceneDelegate`, and the project has `[Register("SceneDelegate")] public class SceneDelegate : MauiUISceneDelegate { }` beside the `AppDelegate`. On macOS 27 an app without it quits at its first window; the stock `dotnet new maui` template has none, so add it when the project is created.
|
|
83
|
+
- **Keychain and signing.** An app that asks for `keychain-access-groups` (MAUI `SecureStorage` needs it) must be signed with a development certificate and provisioning profile (`CodesignKey`, `CodesignProvisioningProfile`), or give Debug builds a second entitlements file without it and a Debug credential store that does not use the keychain. macOS refuses to start a locally signed build that asks for it. Decide which before the first Mac build.
|
|
84
|
+
- **Clean after an update.** After a .NET SDK or MAUI workload update, build the Mac head from clean (`--no-incremental`, or delete its `bin/` and `obj/`): an old precompiled build stops at start in `load_aot_module`.
|
|
85
|
+
- **Launching it.** Open the built `.app` with `open`, never its binary: macOS kills a sandboxed app started from its binary.
|
|
86
|
+
- **Blazor Hybrid.** On a Mac, `data-testid` and HTML `id` do not reach the test driver, so the Mac screens are checked without control names (blank window, error bar, overlap, screenshot) and the control-by-control check is the Windows head's. Keep the stock error bar (`#blazor-error-ui`, "An unhandled error has occurred"), or pass its text with `--error-text`.
|
|
87
|
+
|
|
88
|
+
## 9b. Zoom in Blazor Hybrid heads
|
|
89
|
+
|
|
90
|
+
Every head that draws its screens in a `BlazorWebView` (Mac Catalyst and Windows) zooms with the operating system's keys: Cmd on the Mac, Ctrl on Windows, `=` in, `-` out, `0` actual size, and Ctrl+wheel on Windows. The Mac menu bar shows View › Zoom In, Zoom Out, Actual Size. The level runs from 50% to 300% in 10% steps and is kept in MAUI `Preferences` (key `tf.zoom`) across restarts.
|
|
91
|
+
|
|
92
|
+
- **Copy, do not rewrite.** `.tfcore/standards/blazor-zoom.cs.txt` is the code: copy it to the project as `BlazorZoom.cs`, set its namespace, and add the hooks its header names (one line in the page holding the `BlazorWebView`, four members on the Mac app delegate).
|
|
93
|
+
- **Mac Catalyst.** It sets `WKWebView.PageZoom`. From macOS 26 the main menu ignores what an app delegate's `BuildMenu` changes, so the commands are added through `UIMainMenuSystem.SharedSystem.SetBuildConfiguration`, with the Format menu off: its Font › Bigger and Smaller own Cmd+= and Cmd+-, and UIKit silently drops a menu whose shortcut repeats one.
|
|
94
|
+
- **Windows.** The WinUI `WebView2` control does not expose WebView2's `ZoomFactor`, and WebView2's own zoom steps unevenly from 25% to 500% and keeps nothing. So its zoom control is turned off and a script added to every page handles the keys and the wheel, and posts each level back to be saved.
|
|
95
|
+
|
|
96
|
+
`tf-maccatalyst-check.sh` warns about a Blazor Hybrid head without this; `tf-verify-zoom.sh` presses the keys on a booted head.
|
|
97
|
+
|
|
98
|
+
## 10. Enforcement
|
|
79
99
|
|
|
80
100
|
`.editorconfig` at the repository root (created at day-1) enforces: file-scoped namespaces (warning), `Async` suffix (warning), `var` for locals (warning), nullable enabled, no `_` prefix on private fields (custom naming rule, warning). `StyleCop.Analyzers` is optional and off by default.
|
|
81
101
|
|
|
@@ -90,4 +110,12 @@ grep -rE "public\s+(async\s+)?Task\s+\w+_\w+\s*\(" tests/ 2>/dev/null
|
|
|
90
110
|
grep -rPE "private(\s+readonly)?\s+\w+\s+(?!obj)[A-Z]\w+\s*[;=]" src/ 2>/dev/null | grep -v "static\|const"
|
|
91
111
|
```
|
|
92
112
|
|
|
93
|
-
|
|
113
|
+
A MAUI project with a Mac Catalyst target or a Blazor Hybrid head also runs, on any host (it only reads files):
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
bash .tfcore/utils/tf-maccatalyst-check.sh # FAIL: scene manifest or delegate missing; WARN: keychain on a local build, or a Blazor Hybrid head without zoom (§9b)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`tf-verify-boot.sh --head maccatalyst` runs it first and stops on a FAIL on macOS 27 or later.
|
|
120
|
+
|
|
121
|
+
Severity: error for file-scoped namespace and underscore field prefix; error for a Mac Catalyst FAIL; warning for nullable and the `Async` suffix; the rest informational.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Words the owner should never have to look up, and what to say instead.
|
|
2
|
+
# Read by .tfcore/utils/tf-owner-text.py. One per line: word | say instead
|
|
3
|
+
# A line is added when the owner has had to ask what a word meant, or when the framework's own
|
|
4
|
+
# rules name it (_owner-language.md). Adding a word is cheap; a word the owner knows is a false alarm,
|
|
5
|
+
# so do not add ordinary English or the owner's own vocabulary (parity, checklist, verify, phase).
|
|
6
|
+
oracle | the framework's own script
|
|
7
|
+
denominator | the number you divide by
|
|
8
|
+
numerator | the number being divided
|
|
9
|
+
idempotent | safe to run twice
|
|
10
|
+
invariant | a rule that always holds
|
|
11
|
+
orthogonal | unrelated
|
|
12
|
+
heuristic | a rule of thumb
|
|
13
|
+
canonical | the standard one
|
|
14
|
+
provenance | where the number came from
|
|
15
|
+
cardinality | how many
|
|
16
|
+
semantics | meaning
|
|
17
|
+
deterministic | gives the same answer every time
|
|
18
|
+
tuple | list
|
|
19
|
+
enum | fixed list of values
|
|
20
|
+
rollup | the combined report
|
|
21
|
+
taint | marked unreliable
|
|
22
|
+
short-circuit | stops early
|
|
23
|
+
payload | the data sent
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# _develop-mode (shared rule — *develop-end-to-end: a brief to a UAT-ready app, nobody asked)
|
|
2
|
+
|
|
3
|
+
Develop mode is on while `.tfcore/.session/develop.json` exists. `tf-develop.sh` writes it when `*develop-end-to-end` starts and removes it when the run ends. Everything in `_yolo-mode.md` applies, and on top of it:
|
|
4
|
+
|
|
5
|
+
## What changes
|
|
6
|
+
|
|
7
|
+
- **No question to the owner, ever.** Every question a task would ask is answered here, or by the brief (`docs/{App}-Brief.md`), or decided by you. A decision the owner would normally make is written down where the owner will see it: in the Architecture's Stack Decisions, or in the document it belongs to, under a line starting `Decided in develop mode:`, with one reason. Never stop to ask, and never write `done blocked` for a question you could decide.
|
|
8
|
+
- **The mockups are yours.** In this command you drew them on Day 1, so a difference between a mockup and what the stack mandates (a library's own control, a required layout) is never an owner decision: redraw the mockup to match the mandated component, write `Decided in develop mode:` with the reason in the UIDesign, re-render it, and verify again. The same for any document you wrote in this run. Only a thing the owner alone can supply is a blocker.
|
|
9
|
+
- **No owner-review stop.** This is the one command that crosses the reviews: `*day1-greenfield` stage 1 ends without "review them, then run --stage2"; the supervisor starts stage 2 itself. Everything else about each stage is unchanged.
|
|
10
|
+
- **Git is the supervisor's.** `tf-develop.sh` commits and pushes after each phase from its own shell. You still never run a git write: the guard is unchanged, and you do not need to.
|
|
11
|
+
|
|
12
|
+
## Day 1 answers
|
|
13
|
+
|
|
14
|
+
| Question in `day1-greenfield.md` | Answer in develop mode |
|
|
15
|
+
|---|---|
|
|
16
|
+
| 1. The app name | `{App}` from `develop.json` |
|
|
17
|
+
| 2. The concept | `docs/{App}-Brief.md`, all of it |
|
|
18
|
+
| 3. Hints | none, unless the brief names notes or mockups to harvest |
|
|
19
|
+
| 4. The stack | the stack the brief names; when it names none, the owner's answer set under `.tfcore/templates/stack-defaults/` (the one `core-config.yaml` names, or the only one there). Each question that answer set leaves open is decided as below, and every answer is written into the Architecture's Stack Decisions |
|
|
20
|
+
| 5. The size | the size you propose from your count; a Large brief is built as its phase 1 only, and the other phases are listed in the Build Report as not built |
|
|
21
|
+
|
|
22
|
+
Questions an answer set commonly leaves open:
|
|
23
|
+
|
|
24
|
+
- **Authentication.** The mechanism the brief names. When it names none: the app's own sign-in with local accounts, stored in the app's database; a shared identity platform needs credentials an unattended run does not have.
|
|
25
|
+
- **Rendering or hosting model of the UI.** The one the brief implies; otherwise the answer set's first choice.
|
|
26
|
+
- **Hosting and production secrets.** Not asked: they belong after UAT.
|
|
27
|
+
|
|
28
|
+
## The database
|
|
29
|
+
|
|
30
|
+
When the Architecture names a database server and none is reachable, create it locally and continue: a container named `{app}-db` (lower case) on a free port, with a development password kept in the app's development secrets, never in a committed file. Write the connection details into the UsageGuide's setup section. Create the schema only through the migration path the Stack Decisions name, during `*build-phase` (the database guard allows it there). This overrides the answer set's "never creates its own database container" for this command only.
|
|
31
|
+
|
|
32
|
+
## What does not change
|
|
33
|
+
|
|
34
|
+
The status gate, the smoke policy, the verifier's checks and the guard hooks. A row is `Verified` only from an executed verify. A thing only the owner can supply (a paid account, a device, a credential for a third-party service the brief requires) is still a blocker: finish everything else first, list it under PROJECT-STATUS "Known blockers", then `done blocked`.
|
|
@@ -9,6 +9,7 @@ Everything the owner reads — a terminal reply, a document, a status line, the
|
|
|
9
9
|
- **No coined terms, no shorthand.** "the framework's own script", not "the oracle". "the number you divide by", not "the denominator". If the owner would have to look a word up, it is the wrong word.
|
|
10
10
|
- **Writing a glossary means the words are wrong.** Change the words. `tf-doc-check.sh` refuses a glossary section.
|
|
11
11
|
- First sentence carries the answer. Name the thing, not its id. Short sentences. Never dramatise.
|
|
12
|
+
- **A command's closing message is checked before the turn can end** (`tf-owner-text.sh`, run by the Stop hook on it and on any free-form document it hands over): no word from `.tfcore/standards/owner-words.txt`; each open upstream problem in a table row saying what it affects and whether it blocks or breaks anything, with the prompt that fixes it; one recorded as fixed upstream called fixed; each command still to run given as the line to paste; and the next prompt in a code block.
|
|
12
13
|
|
|
13
14
|
## 2. A question for the owner goes in a file, not in the conversation
|
|
14
15
|
|
|
@@ -7,8 +7,9 @@ Steps, in this order:
|
|
|
7
7
|
1. `bash .tfcore/utils/tf-status-facts.sh {App} "{command}"` prints today's date, the phase line, the last verified build, the Open requirements section, the Verification log with its new row, the library feedback lines, and the next command in both harness forms. Copy them as printed.
|
|
8
8
|
2. Write `PROJECT-STATUS.md` with the Write tool in the shape of `.tfcore/templates/v4custom/app-project-status-tmpl.md`: replace every section in place, add none. You write two parts yourself: "Where I am" (at most 80 words, state not story) and "Known blockers". A refused write means the content is mis-shaped: trim it, do not reword it.
|
|
9
9
|
3. `bash .tfcore/utils/tf-doc-check.sh PROJECT-STATUS.md <every document this command wrote>`; day-1 and `*amend-docs` run `--app {App}` instead, because they own every document. A `FAIL` line means the phase is not closed: fix the document and re-run. A line printed as `OLD` was already there when the command started (the phase marker records them); it does not block and is repaired through `*amend-docs`. Never edit a schema to make a document pass. Documents this command did not write are reported with `--warn`, not fixed here: an older project is repaired through `*amend-docs` when it is next worked on (Schemas §7.1, decision 8).
|
|
10
|
-
4. `bash .tfcore/utils/tf-
|
|
11
|
-
5. `bash .tfcore/utils/tf-
|
|
12
|
-
6.
|
|
10
|
+
4. When this command changed a file under `docs/`, `bash .tfcore/utils/tf-doc-tests.sh <every document it changed>`. A unit test may read a document and guard its text; when one does, this runs the unit tests. `NONE` or `PASS` goes on. `FAIL` means the phase is not closed: put the guarded text back, or, when the change is meant, name the test under "Known blockers" for `*triage-and-fix` to update, since a document command never edits code. `NOT-RUN` is reported as it is printed (Lekhak TF-016).
|
|
11
|
+
5. `bash .tfcore/utils/tf-render-html.sh PROJECT-STATUS.md`, and the same for every human document this command wrote. The checklist, the miss log and feedback files are never rendered.
|
|
12
|
+
6. `bash .tfcore/utils/tf-brd-status.sh {App}` rewrites the BRD's Development status table from the checklist and re-renders the BRD.
|
|
13
|
+
7. Append the run record: `.tfcore/tasks/_metrics-emit-gate.md`.
|
|
13
14
|
|
|
14
15
|
Evidence for every line is the checklist's Requirements Status table and the files on disk, never git. Check a framework file by reading its literal path under `.tfcore/`; search tools skip that folder.
|