@techierathore/techieflow 1.1.1 → 1.2.0
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/tasks/_smoke-test-policy.md +7 -5
- package/.tfcore/hooks/block-git.sh +25 -2
- package/.tfcore/hooks/guard-status-html.sh +5 -1
- package/.tfcore/tasks/_smoke-test-policy.md +7 -5
- package/.tfcore/templates/stack-defaults/dotnet.md +2 -0
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +4 -1
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/tf-brd-status.py +10 -1
- package/.tfcore/utils/tf-checklist-edit.py +8 -1
- package/.tfcore/utils/tf-doc-check.py +41 -0
- package/.tfcore/utils/tf-mockup-parity.mjs +6 -1
- package/.tfcore/utils/tf-owner-text.py +27 -5
- package/.tfcore/utils/tf-triage.py +18 -3
- package/.tfcore/utils/tf-verify-boot.sh +24 -4
- package/.tfcore/utils/tf-verify-screens.mjs +6 -0
- package/.tfcore/utils/tf_feedback.py +7 -2
- package/package.json +1 -1
- package/scripts/install.mjs +1 -1
|
@@ -12,23 +12,25 @@ A screen passes only if all three hold, at a desktop width and a mobile width:
|
|
|
12
12
|
|
|
13
13
|
A failure on any of the three is a defect: write it into the row's Remarks (prefix `⚠ visual:` for 2 and 3) and do not report the row done. A smoke earns a row at most `Implemented`; only an executed verify run writes `Verified`.
|
|
14
14
|
|
|
15
|
-
The evidence of a smoke is the file `bash .tfcore/utils/tf-verify-screens.sh --screen <name>=<route> … --base <url>` writes (`tests/.artifacts/verify/screens.json`, with a screenshot per screen and width); a row is written `Implemented` only after that run, and its Remark names the file. The application is booted with `bash .tfcore/utils/tf-verify-boot.sh start`, which also drives
|
|
15
|
+
The evidence of a smoke is the file `bash .tfcore/utils/tf-verify-screens.sh --screen <name>=<route> … --base <url>` writes (`tests/.artifacts/verify/screens.json`, with a screenshot per screen and width); a row is written `Implemented` only after that run, and its Remark names the file. The application is booted with `bash .tfcore/utils/tf-verify-boot.sh start`, which also drives a Windows head; a head the script cannot boot leaves the row below `Implemented` with the reason.
|
|
16
16
|
|
|
17
17
|
## Run it yourself
|
|
18
18
|
|
|
19
|
-
The build ladder (`.tfcore/templates/v4custom/build-invocation-ladder.md`) says how to build, run and reach the running UI on every supported host, including native and mobile heads. Follow it.
|
|
19
|
+
The build ladder (`.tfcore/templates/v4custom/build-invocation-ladder.md`) says how to build, run and reach the running UI on every supported host, including native and mobile heads. Follow it. Start a dependent service that is down, from its own configuration, in dependency order. Ask the owner to run something only after the ladder's last rung has failed, and then still run the smoke yourself once they answer.
|
|
20
20
|
|
|
21
21
|
## Test users
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Take credentials from the first that works:
|
|
24
24
|
|
|
25
25
|
1. The Test users table in `docs/<App>-UsageGuide.md`.
|
|
26
|
-
2. An existing account in the database, read with the connection string
|
|
26
|
+
2. An existing account in the database, read with the configured connection string.
|
|
27
27
|
3. Ask the owner, showing what you would create.
|
|
28
|
-
4. Only after a yes, create it and add it to the
|
|
28
|
+
4. Only after a yes, create it and add it to the table.
|
|
29
29
|
|
|
30
30
|
Never invent a throwaway account.
|
|
31
31
|
|
|
32
|
+
An AppManager app skips steps 2 to 4 (stack answer Q4a): register through its register call, passwords in git-ignored secrets, never the owner's account.
|
|
33
|
+
|
|
32
34
|
## Say what ran
|
|
33
35
|
|
|
34
36
|
Name the exact command and where it ran: which host, which rung, focused tests or the full build, your own smoke or an executed verify. A green intermediate step is not a pass of the whole.
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
# the only place that decides. Outside YOLO it returns permissionDecision=ask
|
|
22
22
|
# for them (same prompt the owner always had). In YOLO it stays silent and the
|
|
23
23
|
# bare "Bash" allow rule lets them run — an unattended goal run never stalls on
|
|
24
|
-
# a delete.
|
|
24
|
+
# a delete. A delete of the whole disk or home folder (rm -rf / , /* , ~ , ~/*)
|
|
25
|
+
# is refused here in every mode; settings.json deny keeps only the exact
|
|
26
|
+
# `rm -rf /` and `rm -rf ~` (a deny `*` matched every absolute path).
|
|
25
27
|
#
|
|
26
28
|
# YOLO is ON when any of: env TF_YOLO=1 · .tfcore/.session/yolo.json exists
|
|
27
29
|
# (tf-yolo.sh on) · the hook payload's permission_mode is bypassPermissions/auto.
|
|
@@ -198,6 +200,20 @@ def classify_gh(args):
|
|
|
198
200
|
found = {"read": 0, "write": 0}
|
|
199
201
|
found_rm = [False]
|
|
200
202
|
found_sudo = [False]
|
|
203
|
+
found_wipe = [False]
|
|
204
|
+
|
|
205
|
+
# A delete whose target is the whole disk or the home folder is refused in every mode. These
|
|
206
|
+
# lived in settings.json deny as "rm -rf /*" and "rm -rf ~/*", but a deny rule's * matches any
|
|
207
|
+
# text, so they also refused every delete of an absolute path (rm -rf /mnt/c/...), even in
|
|
208
|
+
# bypass mode (Lekhak scratch folder, 2026-10-18).
|
|
209
|
+
HOME_DIR = os.path.expanduser("~").rstrip("/")
|
|
210
|
+
WIPE = {"/", "/*", "/.", "/..", "~", "~/", "~/*", "~/.", "$HOME", "$HOME/", "$HOME/*",
|
|
211
|
+
"${HOME}", "${HOME}/", "${HOME}/*"}
|
|
212
|
+
def is_wipe(t):
|
|
213
|
+
t = t.strip()
|
|
214
|
+
if t in WIPE or re.fullmatch(r"/+\*?", t):
|
|
215
|
+
return True
|
|
216
|
+
return bool(HOME_DIR) and t.rstrip("/*") == HOME_DIR
|
|
201
217
|
|
|
202
218
|
def scan(cmdline, depth=0):
|
|
203
219
|
if depth > 4:
|
|
@@ -263,6 +279,8 @@ def scan(cmdline, depth=0):
|
|
|
263
279
|
found[classify_gh(args)] += 1
|
|
264
280
|
elif head in ("rm", "rmdir", "unlink", "shred"):
|
|
265
281
|
found_rm[0] = True
|
|
282
|
+
if any(is_wipe(a) for a in args if not a.startswith("-") or a == "-"):
|
|
283
|
+
found_wipe[0] = True
|
|
266
284
|
elif head in ("find",) and ("-delete" in args or "-exec" in args and any(basename(x) == "rm" for x in args)):
|
|
267
285
|
found_rm[0] = True
|
|
268
286
|
|
|
@@ -276,7 +294,9 @@ except Exception:
|
|
|
276
294
|
print("ALLOW")
|
|
277
295
|
sys.exit(0)
|
|
278
296
|
|
|
279
|
-
if
|
|
297
|
+
if found_wipe[0]:
|
|
298
|
+
print("BLOCK_WIPE")
|
|
299
|
+
elif found["write"]:
|
|
280
300
|
print("BLOCK_WRITE")
|
|
281
301
|
elif found["read"] and (strict_git or not yolo):
|
|
282
302
|
print("BLOCK_READ")
|
|
@@ -290,6 +310,9 @@ VERDICT="$(TF_HOOK_INPUT="$INPUT" TF_YOLO_ON="$YOLO" python3 -c "$GIT_GUARD_PY"
|
|
|
290
310
|
case "$VERDICT" in
|
|
291
311
|
BLOCK_WRITE) block_git_msg; exit 2 ;;
|
|
292
312
|
BLOCK_READ) block_read_msg; exit 2 ;;
|
|
313
|
+
BLOCK_WIPE)
|
|
314
|
+
echo "BLOCKED by TechieFlow policy: this deletes the whole disk or the home folder. Never run it, in any mode. Delete the folder you mean by its own path." >&2
|
|
315
|
+
exit 2 ;;
|
|
293
316
|
ASK_RM)
|
|
294
317
|
# Claude Code: surface the owner's usual delete/sudo prompt (hook "ask"
|
|
295
318
|
# beats the bare "Bash" allow). OpenCode: its own `rm *: ask` map prompts.
|
|
@@ -149,7 +149,11 @@ if written_this_session:
|
|
|
149
149
|
app = os.path.basename(cl)[: -len("-Checklist.md")]
|
|
150
150
|
brd = os.path.join(root, "docs", f"{app}-BRD.md")
|
|
151
151
|
if os.path.isfile(brd) and os.path.getmtime(cl) > os.path.getmtime(brd):
|
|
152
|
-
|
|
152
|
+
# the stem of a phase file is "<App>-P<n>": the command takes the app and --phase n
|
|
153
|
+
# (Lekhak TF-034, TechieRag TF-007)
|
|
154
|
+
m = re.match(r"^(.+)-P(\d+)$", app)
|
|
155
|
+
cmd = f"{m.group(1)} --phase {m.group(2)}" if m else app
|
|
156
|
+
problems.append(f"docs/{app}-BRD.md is older than docs/{app}-Checklist.md. Run: bash .tfcore/utils/tf-brd-status.sh {cmd}")
|
|
153
157
|
except Exception:
|
|
154
158
|
pass
|
|
155
159
|
|
|
@@ -12,23 +12,25 @@ A screen passes only if all three hold, at a desktop width and a mobile width:
|
|
|
12
12
|
|
|
13
13
|
A failure on any of the three is a defect: write it into the row's Remarks (prefix `⚠ visual:` for 2 and 3) and do not report the row done. A smoke earns a row at most `Implemented`; only an executed verify run writes `Verified`.
|
|
14
14
|
|
|
15
|
-
The evidence of a smoke is the file `bash .tfcore/utils/tf-verify-screens.sh --screen <name>=<route> … --base <url>` writes (`tests/.artifacts/verify/screens.json`, with a screenshot per screen and width); a row is written `Implemented` only after that run, and its Remark names the file. The application is booted with `bash .tfcore/utils/tf-verify-boot.sh start`, which also drives
|
|
15
|
+
The evidence of a smoke is the file `bash .tfcore/utils/tf-verify-screens.sh --screen <name>=<route> … --base <url>` writes (`tests/.artifacts/verify/screens.json`, with a screenshot per screen and width); a row is written `Implemented` only after that run, and its Remark names the file. The application is booted with `bash .tfcore/utils/tf-verify-boot.sh start`, which also drives a Windows head; a head the script cannot boot leaves the row below `Implemented` with the reason.
|
|
16
16
|
|
|
17
17
|
## Run it yourself
|
|
18
18
|
|
|
19
|
-
The build ladder (`.tfcore/templates/v4custom/build-invocation-ladder.md`) says how to build, run and reach the running UI on every supported host, including native and mobile heads. Follow it.
|
|
19
|
+
The build ladder (`.tfcore/templates/v4custom/build-invocation-ladder.md`) says how to build, run and reach the running UI on every supported host, including native and mobile heads. Follow it. Start a dependent service that is down, from its own configuration, in dependency order. Ask the owner to run something only after the ladder's last rung has failed, and then still run the smoke yourself once they answer.
|
|
20
20
|
|
|
21
21
|
## Test users
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Take credentials from the first that works:
|
|
24
24
|
|
|
25
25
|
1. The Test users table in `docs/<App>-UsageGuide.md`.
|
|
26
|
-
2. An existing account in the database, read with the connection string
|
|
26
|
+
2. An existing account in the database, read with the configured connection string.
|
|
27
27
|
3. Ask the owner, showing what you would create.
|
|
28
|
-
4. Only after a yes, create it and add it to the
|
|
28
|
+
4. Only after a yes, create it and add it to the table.
|
|
29
29
|
|
|
30
30
|
Never invent a throwaway account.
|
|
31
31
|
|
|
32
|
+
An AppManager app skips steps 2 to 4 (stack answer Q4a): register through its register call, passwords in git-ignored secrets, never the owner's account.
|
|
33
|
+
|
|
32
34
|
## Say what ran
|
|
33
35
|
|
|
34
36
|
Name the exact command and where it ran: which host, which rung, focused tests or the full build, your own smoke or an executed verify. A green intermediate step is not a pass of the whole.
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
| Q2 | Secrets in development | Project user secrets (`dotnet user-secrets`), visible to the developer, never committed. `appsettings.Development.json` is committed and therefore never holds a real secret. A `secrets.example.json` in the project lists every key the application reads. | No |
|
|
18
18
|
| Q3 | Database | PostgreSQL, running in Docker, for development and production. Docker is always running on the development machine because WSL, where the harness runs, depends on it. If PostgreSQL is unreachable the container is stopped: the agent starts it and continues. Only if no PostgreSQL container exists at all does the agent stop and ask the owner for connection details. It never substitutes another engine and never creates its own database container. | No, unless no container exists |
|
|
19
19
|
| Q4 | Authentication | Two choices only: **AppManager**, the author's shared platform for identity, roles, licences and subscriptions, which counts as part of the application and not as an external integration; or a different mechanism named by the owner. | **Yes**: "AppManager, or something else?" |
|
|
20
|
+
| Q4a | Test users of an AppManager app | An app that signs in through AppManager registers its own test users on AppManager, through AppManager's register call (`POST /AuthSvc/register`, or `device-register` for a device-bound app), under the app's own application id: one for the owner's walkthrough (`{app}-owner@…`), a second user (`{app}-second@…`), and one for unattended runs (`{app}-yolo-tester@…`). A script in the project does it (for example `tests/verify/register-test-users.mjs`): it generates each password and writes it only to the project's git-ignored secrets (user secrets, or a git-ignored `.env.local`). The UsageGuide's Test users table lists the three, names where each password is kept, and never holds a password. An agent never asks for, stores or uses the owner's personal AppManager account. `tf-doc-check.sh` holds the UsageGuide to this. | No |
|
|
20
21
|
| Q5 | Logging | Serilog, always, with file logging wired at startup before anything else can fail. | No |
|
|
21
22
|
| Q6 | Tests | xUnit, always. A test project exists from day-1. | No |
|
|
22
23
|
| Q7 | Layout and naming | `src/` and `tests/` at the root. The primary executable project is named exactly `<App>`; `<App>.App` is banned. Secondary heads take a descriptive suffix (`<App>.Api`, `<App>.Desktop`). | No |
|
|
@@ -32,6 +33,7 @@ Required packages: `TrBlazeUI.Components` — for a project with a Blazor or MAU
|
|
|
32
33
|
## Notes
|
|
33
34
|
|
|
34
35
|
- Q4 is asked on purpose even with this answer set, because the choice differs per product.
|
|
36
|
+
- Q4a follows from an AppManager answer to Q4 and is never asked (owner, 2026-10-18).
|
|
35
37
|
- Q3's "start the container, never create one" replaces the behaviour that produced TfLens's own database container: an agent that cannot reach the expected database restarts it or asks, and never builds an alternative.
|
|
36
38
|
- Q11.5 replaces the `database` folder of loose migration scripts that TfLens accumulated.
|
|
37
39
|
- Q11.3 is a standing framework convention restated as a stack rule so that it is recorded per project and its violations can be counted.
|
|
@@ -13,13 +13,16 @@ entries: How to test, screen by screen |
|
|
|
13
13
|
per-entry: 90 120
|
|
14
14
|
budget: S 2500 3500 | M 4000 6000 | L 4000 6000
|
|
15
15
|
rule: test-users-table
|
|
16
|
+
rule: appmanager-test-users
|
|
16
17
|
rule: execution-code
|
|
17
18
|
rule: entry-steps
|
|
18
19
|
rule: entry-expected
|
|
19
20
|
-->
|
|
20
21
|
<!-- Authoring notes (agent only; never visible text).
|
|
21
22
|
The owner's test plan. Every agent (smoke, verify) and the owner's UAT use the SAME test users and
|
|
22
|
-
the SAME walkthrough; nobody invents accounts.
|
|
23
|
+
the SAME walkthrough; nobody invents accounts. An app that signs in through AppManager lists its own three users
|
|
24
|
+
(owner walkthrough, second user, unattended runs), registered through AppManager's register call, each password named by
|
|
25
|
+
where the git-ignored secrets keep it, never written here; never the owner's personal AppManager account. One `###` per screen in navigation order, at most
|
|
23
26
|
120 words: who to sign in as, numbered steps, the expected result, the REQ ids covered.
|
|
24
27
|
The Execution guide is copy-pasteable commands, one per line, no narrative. Hosting and production
|
|
25
28
|
deployment are not here; they are in the Deployment Checklist after UAT.
|
|
Binary file
|
|
Binary file
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
On a Large project the pair is the phase being worked: --phase N, else appPhase in
|
|
7
7
|
.tfcore/core-config.yaml (phase 1 is docs/<App>-BRD.md and -Checklist.md, phase n is
|
|
8
|
-
docs/<App>-Pn-BRD.md and docs/<App>-Pn-Checklist.md).
|
|
8
|
+
docs/<App>-Pn-BRD.md and docs/<App>-Pn-Checklist.md). A name that carries the phase, as the file
|
|
9
|
+
stem does (<App>-P3), is read as <App> --phase 3.
|
|
9
10
|
|
|
10
11
|
Reads docs/<App>-Checklist.md: every REQ row of the Requirements Status table, and
|
|
11
12
|
the "## Page: …" section its detail entry sits under, which names the screen.
|
|
@@ -127,10 +128,18 @@ def main(argv):
|
|
|
127
128
|
render = "--no-render" not in argv
|
|
128
129
|
root = os.getcwd()
|
|
129
130
|
phase = None
|
|
131
|
+
# A name that already carries the phase, as the file stem reads ("Lekhak-P3"), is the app and that
|
|
132
|
+
# phase: adding appPhase again looked for docs/Lekhak-P3-P3-Checklist.md (Lekhak TF-034,
|
|
133
|
+
# TechieRag TF-007).
|
|
134
|
+
m = re.match(r"^(.+)-P(\d+)$", app)
|
|
135
|
+
if m:
|
|
136
|
+
app, phase = m.group(1), int(m.group(2))
|
|
130
137
|
if "--phase" in argv:
|
|
131
138
|
i = argv.index("--phase")
|
|
132
139
|
if i + 1 >= len(argv) or not argv[i + 1].isdigit() or int(argv[i + 1]) < 1:
|
|
133
140
|
die("--phase needs a number from 1 upwards")
|
|
141
|
+
if phase is not None and phase != int(argv[i + 1]):
|
|
142
|
+
die(f"{argv[1]} names phase {phase} but --phase says {argv[i + 1]}")
|
|
134
143
|
phase = int(argv[i + 1])
|
|
135
144
|
if phase is None:
|
|
136
145
|
cfg = os.path.join(root, ".tfcore", "core-config.yaml")
|
|
@@ -86,6 +86,13 @@ def siblings(path):
|
|
|
86
86
|
return [os.path.join(d, n) for n in sorted(names) if n != f and re.match(rf"^{app}(?:-P\d+)?-Checklist\.md$", n)]
|
|
87
87
|
|
|
88
88
|
|
|
89
|
+
def streams(path):
|
|
90
|
+
"""the telemetry streams beside the checklists: an id a miss or gate record carries is taken even
|
|
91
|
+
after its row was renamed, since a record cannot be amended (TechieRag TF-005)"""
|
|
92
|
+
d = os.path.join(os.path.dirname(path) or ".", "metrics")
|
|
93
|
+
return [os.path.join(d, n) for n in ("gates.jsonl", "misses.jsonl", "runs.jsonl") if os.path.isfile(os.path.join(d, n))]
|
|
94
|
+
|
|
95
|
+
|
|
89
96
|
def next_id(text, prefix, others=()):
|
|
90
97
|
"""one past the highest id of that prefix in this checklist AND every other checklist of the app:
|
|
91
98
|
counting this one alone gave REQ-UI-055 twice, once in phase 1 and once in phase 3 (Chatur TF-011)"""
|
|
@@ -99,7 +106,7 @@ def add_row(path, prefix, title, acceptance, section=None, remark=None, status="
|
|
|
99
106
|
"""append a row to the Requirements Status table and a detail entry under `section`
|
|
100
107
|
(a new section at the end when it does not exist); returns the new id"""
|
|
101
108
|
text = read(path)
|
|
102
|
-
rid = next_id(text, prefix, siblings(path))
|
|
109
|
+
rid = next_id(text, prefix, siblings(path) + streams(path))
|
|
103
110
|
nl = "\r\n" if "\r\n" in text else "\n"
|
|
104
111
|
lines = text.split(nl)
|
|
105
112
|
# the table: after the last REQ row
|
|
@@ -30,6 +30,7 @@ Schema grammar (one `key: value` per line inside the comment):
|
|
|
30
30
|
from __future__ import annotations
|
|
31
31
|
|
|
32
32
|
import argparse
|
|
33
|
+
import glob
|
|
33
34
|
import json
|
|
34
35
|
import os
|
|
35
36
|
import re
|
|
@@ -412,6 +413,17 @@ def resolve_kind(header, brd_header, cfg) -> str:
|
|
|
412
413
|
return "library" if raw.startswith("lib") else "app"
|
|
413
414
|
|
|
414
415
|
|
|
416
|
+
def signs_in_through_appmanager(root, app):
|
|
417
|
+
"""the Architecture's authentication row (Q4, Auth…, Identity) names AppManager"""
|
|
418
|
+
for f in glob.glob(os.path.join(root or ".", "docs", f"{app}-Architecture.md")):
|
|
419
|
+
for _h, rows in tables_in(open(f, encoding="utf-8", errors="replace").read()):
|
|
420
|
+
for r in rows:
|
|
421
|
+
head = " ".join(r[:2]).lower()
|
|
422
|
+
if re.search(r"\bq4\b|auth|identity|sign-in", head) and re.search(r"(?i)app ?manager", " ".join(r[1:3])):
|
|
423
|
+
return True
|
|
424
|
+
return False
|
|
425
|
+
|
|
426
|
+
|
|
415
427
|
def section_text(present, name):
|
|
416
428
|
key = norm_heading(name)
|
|
417
429
|
return next((txt for k, _h, txt in present if key_matches(key, k)), None)
|
|
@@ -799,6 +811,35 @@ def check_doc_rule(rule, c, rep):
|
|
|
799
811
|
if txt is not None and not has_table_with(txt, "user", "role"):
|
|
800
812
|
rep.fail(rel, 'the "Test users" section needs a table with User and Role columns')
|
|
801
813
|
|
|
814
|
+
elif rule == "appmanager-test-users":
|
|
815
|
+
# An app that signs in through AppManager has its own three test users there, registered through
|
|
816
|
+
# AppManager's register call, each password kept in the git-ignored secrets and never written in
|
|
817
|
+
# the guide; never the owner's personal account (stack answer Q4a, owner 2026-10-18).
|
|
818
|
+
txt = section_text(present, "Test users")
|
|
819
|
+
app = c.get("app")
|
|
820
|
+
if txt is None or c["kind"] != "app" or not app or app.lower() == "appmanager" or not signs_in_through_appmanager(c["root"], app):
|
|
821
|
+
return
|
|
822
|
+
users = []
|
|
823
|
+
for hdr, rows in tables_in(txt):
|
|
824
|
+
low = [h.lower() for h in hdr]
|
|
825
|
+
if not any("user" in h for h in low):
|
|
826
|
+
continue
|
|
827
|
+
pw = next((i for i, h in enumerate(low) if "password" in h), None)
|
|
828
|
+
users += [(r, r[pw] if pw is not None and pw < len(r) else "") for r in rows if "@" in " ".join(r)]
|
|
829
|
+
whole = lambda r: " ".join(r).lower()
|
|
830
|
+
want = (("owner", r"owner|account holder"), ("second user", r"second"), ("unattended-run user", r"unattended|yolo"))
|
|
831
|
+
missing = [n for n, pat in want if not any(re.search(pat, whole(r)) for r, _p in users)]
|
|
832
|
+
if missing:
|
|
833
|
+
rep.fail(rel, f'{app} signs in through AppManager: the Test users table lists no {", no ".join(missing)} '
|
|
834
|
+
f'(stack answer Q4a: owner walkthrough, second user, unattended runs, registered through AppManager\'s register call)')
|
|
835
|
+
for r, cell in users:
|
|
836
|
+
if not re.search(r"(?i)secret|\.env|credential|keychain", cell):
|
|
837
|
+
who = re.search(r"[\w.+-]+@[\w-]+(?:\.[\w-]+)*", " ".join(r)).group(0)
|
|
838
|
+
rep.fail(rel, f'test user {who}: the Password cell names no git-ignored secret; keep the password in the project\'s '
|
|
839
|
+
f'secrets and name the key here, never the password (stack answer Q4a)')
|
|
840
|
+
if users and not re.search(r"(?i)regist", txt):
|
|
841
|
+
rep.fail(rel, "the Test users section does not say the users were registered through AppManager's register call (stack answer Q4a)")
|
|
842
|
+
|
|
802
843
|
elif rule == "next-command-blocks":
|
|
803
844
|
seg = re.search(r"(?ms)^## Next command to run\s*\n(.*?)(?=^## |\Z)", nocomment)
|
|
804
845
|
if not seg:
|
|
@@ -275,7 +275,12 @@ const PROBE = (wanted = []) => {
|
|
|
275
275
|
// reads every other border property but not this one keeps missing that whole
|
|
276
276
|
// vocabulary. Quantised to the three values that carry meaning rather than the
|
|
277
277
|
// full CSS keyword set, so the clause stays as noise-free as the colour buckets.
|
|
278
|
+
// A checkbox, switch or radio is not compared by its border (Lekhak TF-035): a native
|
|
279
|
+
// <input type="checkbox"> computes border-style none because the browser draws its box, while a
|
|
280
|
+
// library's role="checkbox" draws the same box as a CSS border. Same control, so not applicable.
|
|
281
|
+
const TOGGLE = 'input[type="checkbox"],input[type="radio"],[role="checkbox"],[role="switch"],[role="radio"]';
|
|
278
282
|
const strokeOf = (el) => {
|
|
283
|
+
if (el.matches(TOGGLE)) return null;
|
|
279
284
|
const cs = getComputedStyle(el);
|
|
280
285
|
const q = (s) => (s === 'none' || s === 'hidden' ? 'none'
|
|
281
286
|
: s === 'dashed' || s === 'dotted' ? 'dashed' : 'solid');
|
|
@@ -465,7 +470,7 @@ const PROBE = (wanted = []) => {
|
|
|
465
470
|
// the badge and the visible border one element away, for the clauses to accept (TF-047)
|
|
466
471
|
near: {
|
|
467
472
|
badge: near.some((n) => chromeOn(n)?.badge === true),
|
|
468
|
-
stroke: near.map(strokeOf).find((s) => s.visible) || null,
|
|
473
|
+
stroke: near.map(strokeOf).find((s) => s && s.visible) || null,
|
|
469
474
|
},
|
|
470
475
|
tag: el.tagName.toLowerCase(),
|
|
471
476
|
text: ownText(el).trim().replace(/\s+/g, ' ').slice(0, 60),
|
|
@@ -59,13 +59,31 @@ def load_words(root):
|
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
def upstream_state(root):
|
|
62
|
-
"""Every entry id in the project's feedback files -> (state, where),
|
|
63
|
-
every script uses (tf_feedback.py), so this check and PROJECT-STATUS can never disagree."""
|
|
62
|
+
"""Every entry id in the project's feedback files -> [(state, where, names)], one per file, from
|
|
63
|
+
the one reader every script uses (tf_feedback.py), so this check and PROJECT-STATUS can never disagree."""
|
|
64
64
|
sys.path.insert(0, HERE)
|
|
65
65
|
import tf_feedback
|
|
66
66
|
return tf_feedback.state_map(root)
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def resolve(rid, cands, unit, text):
|
|
70
|
+
"""The feedback file an id in this unit means (TrBlazeUI TF-006). One file holding it: that one.
|
|
71
|
+
Several: the files whose project the unit names ("Lekhak TR-002"), else the files the whole text
|
|
72
|
+
names. -> (state, where), or (None, the files) when it stays several that disagree."""
|
|
73
|
+
if len(cands) == 1:
|
|
74
|
+
return cands[0][:2]
|
|
75
|
+
for txt in (unit, text):
|
|
76
|
+
# a name every candidate file shares (the library's own) says nothing about which one
|
|
77
|
+
shared = set.intersection(*(set(c[2]) for c in cands))
|
|
78
|
+
hit = [c for c in cands if any(n not in shared and re.search(rf"(?<![\w-]){re.escape(n)}(?![\w-])", txt) for n in c[2])]
|
|
79
|
+
if hit:
|
|
80
|
+
cands = hit
|
|
81
|
+
break
|
|
82
|
+
if len({c[0] for c in cands}) == 1:
|
|
83
|
+
return cands[0][:2]
|
|
84
|
+
return None, ", ".join(c[1].split(",")[0] for c in cands)
|
|
85
|
+
|
|
86
|
+
|
|
69
87
|
def split_text(text):
|
|
70
88
|
"""-> (prose lines [(n, line)], the text inside code blocks)."""
|
|
71
89
|
prose, fenced, fence = [], [], None
|
|
@@ -142,9 +160,13 @@ def check(text, label, root, final=False, words=None, ups=None):
|
|
|
142
160
|
for n, txt, header in us:
|
|
143
161
|
for rid in dict.fromkeys(ANY_ID.findall(txt)):
|
|
144
162
|
if rid in ups:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
163
|
+
st, where = resolve(rid, ups[rid], txt, text)
|
|
164
|
+
if st is None:
|
|
165
|
+
fail(n, f'{rid} is an entry in several feedback files ({where}); name whose it is with the id, '
|
|
166
|
+
f'as in "{ups[rid][0][2][0]} {rid}"')
|
|
167
|
+
continue
|
|
168
|
+
mentions.setdefault((rid, st, where), []).append((n, txt, header))
|
|
169
|
+
for (rid, st, where), ms in sorted(mentions.items(), key=lambda kv: kv[1][0][0]):
|
|
148
170
|
if st in ("fixed", "closed"):
|
|
149
171
|
# read the sentence that names it, or its table row: "TF-901 is filed. X is fixed." is wrong
|
|
150
172
|
said = lambda txt, h: [txt] if h is not None else [s for s in re.split(r"(?<=[.!?])\s+", txt) if rid in s]
|
|
@@ -13,8 +13,10 @@ row in whichever docs/<App>-Checklist.md or docs/<App>-P<N>-Checklist.md holds i
|
|
|
13
13
|
|
|
14
14
|
demote the row goes to Needs re-verify (% capped at 75) with a dated Remark holding the symptom in the
|
|
15
15
|
reporter's words, the evidence path and the kind.
|
|
16
|
-
new a defect no row covers: the next free id in the prefix,
|
|
17
|
-
|
|
16
|
+
new a defect no row covers: the next free id in the prefix, across every checklist of the app and the
|
|
17
|
+
telemetry streams, a Not Started row, a detail entry with the acceptance line and the marker
|
|
18
|
+
BRD-pending (*amend-docs gives it its BRD item). Without --prefix: FN, or the one prefix a
|
|
19
|
+
checklist with no FN row uses; with several such prefixes it asks for --prefix.
|
|
18
20
|
note a dated remark on the nearest row, no status change (could not reproduce).
|
|
19
21
|
close what triage owes the telemetry, from the actions above (tests/.artifacts/verify/triage.json):
|
|
20
22
|
one gate record per demoted or new row with gate "escaped" (no check caught it), one miss per row
|
|
@@ -31,6 +33,7 @@ import datetime
|
|
|
31
33
|
import importlib.util
|
|
32
34
|
import json
|
|
33
35
|
import os
|
|
36
|
+
import re
|
|
34
37
|
import subprocess
|
|
35
38
|
import sys
|
|
36
39
|
|
|
@@ -176,7 +179,19 @@ def main(argv):
|
|
|
176
179
|
die("new needs \"<title>\" \"<acceptance>\"")
|
|
177
180
|
title, acc = argv[3], argv[4]
|
|
178
181
|
cl = checklist(app, argv, ce)
|
|
179
|
-
prefix = opt(argv, "--prefix"
|
|
182
|
+
prefix = opt(argv, "--prefix")
|
|
183
|
+
if prefix is None:
|
|
184
|
+
# FN unless this checklist has no FN row: then the one prefix it uses is the default, and
|
|
185
|
+
# several are asked for (TechieRag TF-005: phase 3 is all RAG, and FN was a slip)
|
|
186
|
+
used = sorted(set(re.findall(r"(?m)^\|\s*REQ-([A-Z]+)-\d{3,}", ce.read(cl))))
|
|
187
|
+
if not used or "FN" in used:
|
|
188
|
+
prefix = "FN"
|
|
189
|
+
elif len(used) == 1:
|
|
190
|
+
prefix = used[0]
|
|
191
|
+
print(f"note: every row of {cl} is REQ-{prefix}-…, so the new row is one too; pass --prefix to choose another")
|
|
192
|
+
else:
|
|
193
|
+
die(f"{cl} has no REQ-FN rows; name the prefix: --prefix {'|'.join(used)}")
|
|
194
|
+
prefix = prefix.upper(); section = opt(argv, "--section"); ev = opt(argv, "--evidence")
|
|
180
195
|
src = opt(argv, "--source", "owner"); mockup = opt(argv, "--mockup")
|
|
181
196
|
if prefix == "UI" and not mockup and cfg_project_type() not in ("library", "docs"): # TrBlazeUI TF-003
|
|
182
197
|
print("note: a UI row needs a mockup link; pass --mockup docs/mockups/<screen>.html or the checker will flag it")
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
# side that built it, reading its settings from the project folder as `dotnet run` does.
|
|
14
14
|
# Builders side by side never serve each other's half-written build (TF-043). A standalone
|
|
15
15
|
# WebAssembly project is still started with `dotnet run`. Prints BOOTED mode=base url=….
|
|
16
|
-
# windows a MAUI Blazor Hybrid head (UseMaui + a net*-windows target
|
|
16
|
+
# windows a MAUI Blazor Hybrid head (UseMaui + a net*-windows target + a BlazorWebView; a plain MAUI
|
|
17
|
+
# app is refused at once, NONE kind=no-webview, TechieRag TF-006): started Windows-side with
|
|
17
18
|
# WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=9222, the DevTools port
|
|
18
19
|
# relayed to every interface by tf-cdp-relay.ps1, and reached from WSL over CDP. Prints
|
|
19
20
|
# BOOTED mode=cdp url=http://<host>:9223. Proven on MyDiary, 2026-09-06.
|
|
@@ -85,6 +86,13 @@ find_projects() { # prints csproj paths under src/, source/, the root and one le
|
|
|
85
86
|
}
|
|
86
87
|
is_web() { grep -qE 'Microsoft\.NET\.Sdk\.Web|Microsoft\.NET\.Sdk\.BlazorWebAssembly' "$1"; }
|
|
87
88
|
is_maui_win() { grep -qiE '<UseMaui>[[:space:]]*true|Microsoft\.NET\.Sdk\.Maui' "$1" && grep -qE 'net[0-9.]+-windows' "$1"; }
|
|
89
|
+
# A Windows head is driven over its web view's DevTools port, so only a Blazor Hybrid app has one. A
|
|
90
|
+
# plain MAUI app (TechieRag's samples/TechieRag.Probe) was picked as the windows head and waited the
|
|
91
|
+
# full 300 s for a port it can never open (TechieRag TF-006).
|
|
92
|
+
is_hybrid() {
|
|
93
|
+
grep -qE 'Microsoft\.AspNetCore\.Components\.WebView\.Maui' "$1" && return 0
|
|
94
|
+
grep -rqlE 'AddMauiBlazorWebView|BlazorWebView' --include='*.cs' --include='*.xaml' --exclude-dir=bin --exclude-dir=obj "$(dirname "$1")" 2>/dev/null
|
|
95
|
+
}
|
|
88
96
|
is_maui_mac() { grep -qiE '<UseMaui>[[:space:]]*true|Microsoft\.NET\.Sdk\.Maui' "$1" && grep -qE 'net[0-9.]+-maccatalyst' "$1"; }
|
|
89
97
|
|
|
90
98
|
poll_http() { # url seconds pid-to-watch(optional) -> 0 when it answers
|
|
@@ -285,7 +293,11 @@ fi
|
|
|
285
293
|
# ---- pick the project and the head ------------------------------------------------------
|
|
286
294
|
tf_read_lines ALL < <(find_projects)
|
|
287
295
|
WEB=(); WIN=(); MAC=()
|
|
288
|
-
|
|
296
|
+
WIN_NATIVE=()
|
|
297
|
+
for p in ${ALL[@]+"${ALL[@]}"}; do
|
|
298
|
+
is_web "$p" && WEB+=("$p"); is_maui_mac "$p" && MAC+=("$p")
|
|
299
|
+
if is_maui_win "$p"; then if is_hybrid "$p"; then WIN+=("$p"); else WIN_NATIVE+=("$p"); fi; fi
|
|
300
|
+
done
|
|
289
301
|
# a Mac Catalyst head is picked unasked only on a Mac, the one host that runs it
|
|
290
302
|
[[ "$PLATFORM" == "macos" ]] || MAC=()
|
|
291
303
|
# Several web projects: the one that serves screens. The first in sorted order booted AppManager's
|
|
@@ -325,9 +337,14 @@ if [[ -n "$PROJECT" ]]; then
|
|
|
325
337
|
elif [[ -z "$HEAD" ]]; then
|
|
326
338
|
if [[ ${#WEB[@]} -gt 0 ]]; then HEAD=web; pick_web
|
|
327
339
|
elif [[ ${#MAC[@]} -gt 0 ]]; then HEAD=maccatalyst; PROJECT="${MAC[0]}"
|
|
328
|
-
elif [[ ${#WIN[@]} -gt 0 ]]; then HEAD=windows; PROJECT="${WIN[0]}"
|
|
340
|
+
elif [[ ${#WIN[@]} -gt 0 ]]; then HEAD=windows; PROJECT="${WIN[0]}"
|
|
341
|
+
elif [[ ${#WIN_NATIVE[@]} -gt 0 ]]; then HEAD=windows; PROJECT="${WIN_NATIVE[0]}"; fi
|
|
329
342
|
else
|
|
330
|
-
case "$HEAD" in web) pick_web ;; windows) PROJECT="${WIN[0]:-}" ;; maccatalyst) PROJECT="${MAC[0]:-}" ;; esac
|
|
343
|
+
case "$HEAD" in web) pick_web ;; windows) PROJECT="${WIN[0]:-${WIN_NATIVE[0]:-}}" ;; maccatalyst) PROJECT="${MAC[0]:-}" ;; esac
|
|
344
|
+
fi
|
|
345
|
+
if [[ "$HEAD" == windows && -n "$PROJECT" ]] && ! is_hybrid "$PROJECT"; then
|
|
346
|
+
[[ $DRYRUN -eq 1 ]] || write_state windows none "" "" "" "$PROJECT" "a MAUI app with no BlazorWebView: no web view, not drivable over CDP" no-webview "$PLATFORM"
|
|
347
|
+
echo "NONE head=windows kind=no-webview reason=$PROJECT is a MAUI app with no BlazorWebView (no AddMauiBlazorWebView, no <BlazorWebView>): it has no web view, so it is not drivable over CDP; its rows are graded by their tests"; exit 2
|
|
331
348
|
fi
|
|
332
349
|
if [[ $DRYRUN -eq 1 ]]; then echo "PICK head=${HEAD:-none} project=${PROJECT:-none}"; exit 0; fi
|
|
333
350
|
case "$HEAD" in
|
|
@@ -714,6 +731,9 @@ MINE="$(comm -13 <(printf '%s\n' "$BEFORE_IDS") <(win_ids) | tr '\n' ' ')"
|
|
|
714
731
|
if [[ -z "$URL" ]]; then
|
|
715
732
|
pkill -P "$APP_PID" 2>/dev/null; kill "$APP_PID" "$RELAY_PID" 2>/dev/null
|
|
716
733
|
for w in $MINE; do taskkill.exe /F /T /PID "$w" >/dev/null 2>&1; done
|
|
734
|
+
# the relay runs Windows-side and outlives its WSL parent: a second try then failed "Only one usage
|
|
735
|
+
# of each socket address" (TechieRag TF-006)
|
|
736
|
+
powershell.exe -NoProfile -Command "Get-NetTCPConnection -LocalPort $RELAY_PORT -State Listen -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id \$_.OwningProcess -Force -ErrorAction SilentlyContinue }" >/dev/null 2>&1
|
|
717
737
|
write_state windows none "" "" "cmd.exe /c dotnet run -f $TFM" "$PROJECT" "the DevTools port $RELAY_PORT never answered within 300 s" host "$PLATFORM"
|
|
718
738
|
echo "NONE head=windows kind=host reason=the app's DevTools port never answered on ${HOSTS[*]}:$RELAY_PORT within 300 s (log $LOG, relay $DIR/relay.log)"; exit 2
|
|
719
739
|
fi
|
|
@@ -366,8 +366,14 @@ async function inspect(page, anchors, attr, errorSelectors) {
|
|
|
366
366
|
return !/^(transparent|rgba\(0, 0, 0, 0\))$/.test(cs.backgroundColor) || cs.backgroundImage !== 'none'
|
|
367
367
|
|| ['Top', 'Right', 'Bottom', 'Left'].some((s) => parseFloat(cs[`border${s}Width`]) > 0 && cs[`border${s}Style`] !== 'none');
|
|
368
368
|
};
|
|
369
|
+
// A checkbox, switch or radio drawn by a library (TrBlazeUI: <button role="checkbox"
|
|
370
|
+
// aria-checked="false">, no text, no child while unticked) paints its own box, the way a native
|
|
371
|
+
// <input type="checkbox"> does (Lekhak TF-035). Counted filled when it paints; an unpainted one
|
|
372
|
+
// with no size is still empty.
|
|
373
|
+
const toggle = '[role="checkbox"],[role="switch"],[role="radio"],[aria-checked]';
|
|
369
374
|
const filled = text.length > 0 || ['input', 'select', 'textarea', 'img', 'svg', 'canvas', 'video', 'iframe', 'progress', 'meter'].includes(tag)
|
|
370
375
|
|| el.matches(bar) || el.querySelector(`img,svg,canvas,input,select,textarea,button,a,${bar}`) !== null
|
|
376
|
+
|| (el.matches(toggle) && paints(el)) || [...el.querySelectorAll(toggle)].some(paints)
|
|
371
377
|
|| [...el.children].some(paints);
|
|
372
378
|
const c = clipRect(el);
|
|
373
379
|
out.anchors.push({ id, present: true, visible: vis(el), filled, tag, w: Math.round(r.width), h: Math.round(r.height), x: Math.round(r.left), y: Math.round(r.top),
|
|
@@ -195,12 +195,17 @@ def upstream_of(path, app=None):
|
|
|
195
195
|
|
|
196
196
|
|
|
197
197
|
def state_map(root):
|
|
198
|
-
"""Every entry id across the project's feedback files -> (state, "<file>, <date>")
|
|
198
|
+
"""Every entry id across the project's feedback files -> [(state, "<file>, <date>", names)], one
|
|
199
|
+
item per file holding it. Every consumer numbers its own entries from 1, so a library with nine
|
|
200
|
+
consumer files can hold nine TR-002s (TrBlazeUI TF-006); `names` are the two project names in the
|
|
201
|
+
file's name (Lekhak and TrBlazeUI in Lekhak-TrBlazeUI-Feedback.md), which a line names to say whose."""
|
|
199
202
|
out = {}
|
|
200
203
|
for f in files(root):
|
|
201
204
|
rel = os.path.relpath(f, root).replace(os.sep, "/")
|
|
205
|
+
names = tuple(os.path.basename(f)[: -len("-Feedback.md")].split("-"))
|
|
202
206
|
for e in entries(f):
|
|
203
|
-
out
|
|
207
|
+
out.setdefault(e["id"], []).append(
|
|
208
|
+
(e["state"], f"{rel}" + (f", Resolution status {e['since']}" if e["since"] else ""), names))
|
|
204
209
|
return out
|
|
205
210
|
|
|
206
211
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@techierathore/techieflow",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "TechieFlow: a spec-driven development framework for AI coding agents, for Claude Code and OpenCode. One command installs it into any project.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
package/scripts/install.mjs
CHANGED
|
@@ -313,7 +313,7 @@ const canonicalSettings = String.raw`{
|
|
|
313
313
|
],
|
|
314
314
|
"ask": [],
|
|
315
315
|
"deny": [
|
|
316
|
-
"Bash(rm -rf /)", "Bash(rm -rf
|
|
316
|
+
"Bash(rm -rf /)", "Bash(rm -rf ~)",
|
|
317
317
|
"Bash(git commit*)", "Bash(git push*)", "Bash(git add*)", "Bash(git rm*)",
|
|
318
318
|
"Bash(git mv*)", "Bash(git reset*)", "Bash(git checkout*)", "Bash(git switch*)",
|
|
319
319
|
"Bash(git restore*)", "Bash(git merge*)", "Bash(git rebase*)", "Bash(git cherry-pick*)",
|