@techierathore/techieflow 1.1.0 → 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/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/_smoke-test-policy.md +7 -5
- package/.claude/commands/TechieFlow/tasks/_yolo-mode.md +2 -2
- package/.claude/commands/TechieFlow/tasks/amend-docs.md +1 -1
- package/.claude/commands/TechieFlow/tasks/day1-brownfield.md +2 -2
- package/.claude/commands/TechieFlow/tasks/day1-greenfield.md +4 -4
- package/.claude/commands/TechieFlow/tasks/develop-end-to-end.md +30 -0
- package/.claude/commands/TechieFlow/tasks/fix-issues.md +1 -0
- package/.claude/commands/TechieFlow/tasks/mockups.md +1 -1
- package/.claude/commands/TechieFlow/tasks/triage-and-fix.md +2 -2
- package/.claude/commands/TechieFlow/tasks/verify-phase.md +12 -11
- package/.opencode/plugin/techieflow.js +15 -7
- 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 +25 -2
- package/.tfcore/hooks/guard-db.sh +106 -9
- package/.tfcore/hooks/guard-status-html.sh +5 -1
- package/.tfcore/hooks/guard-verify-deps.sh +16 -1
- 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/tasks/_develop-mode.md +34 -0
- package/.tfcore/tasks/_smoke-test-policy.md +7 -5
- package/.tfcore/tasks/_yolo-mode.md +2 -2
- package/.tfcore/tasks/amend-docs.md +1 -1
- package/.tfcore/tasks/day1-brownfield.md +2 -2
- package/.tfcore/tasks/day1-greenfield.md +4 -4
- package/.tfcore/tasks/develop-end-to-end.md +30 -0
- package/.tfcore/tasks/fix-issues.md +1 -0
- package/.tfcore/tasks/mockups.md +1 -1
- package/.tfcore/tasks/triage-and-fix.md +2 -2
- package/.tfcore/tasks/verify-phase.md +12 -11
- package/.tfcore/telemetry/SCHEMA.md +6 -2
- package/.tfcore/telemetry/tf-metrics.sh +19 -0
- package/.tfcore/templates/stack-defaults/dotnet.md +5 -1
- package/.tfcore/templates/v4custom/app-architecture-tmpl.md +5 -0
- package/.tfcore/templates/v4custom/app-brd-tmpl.md +2 -1
- package/.tfcore/templates/v4custom/app-checklist-tmpl.md +11 -0
- package/.tfcore/templates/v4custom/app-phases-tmpl.md +2 -1
- package/.tfcore/templates/v4custom/app-usageguide-tmpl.md +4 -1
- package/.tfcore/templates/v4custom/html-render-shell.md +2 -0
- package/.tfcore/utils/__pycache__/tf-checklist-edit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-devguide-list.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-doc-check.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-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-brd-status.py +10 -1
- package/.tfcore/utils/tf-build-list.py +16 -1
- package/.tfcore/utils/tf-build.sh +75 -0
- package/.tfcore/utils/tf-checklist-edit.py +28 -3
- package/.tfcore/utils/tf-day1-files.py +55 -28
- package/.tfcore/utils/tf-develop.sh +264 -0
- package/.tfcore/utils/tf-devguide-list.py +6 -2
- package/.tfcore/utils/tf-doc-check.py +124 -21
- package/.tfcore/utils/tf-doc-tests.sh +40 -3
- package/.tfcore/utils/tf-emit.sh +40 -10
- package/.tfcore/utils/tf-fix-close.sh +13 -7
- package/.tfcore/utils/tf-maccatalyst-check.sh +92 -0
- package/.tfcore/utils/tf-menus.mjs +401 -0
- package/.tfcore/utils/tf-mockup-parity.mjs +361 -28
- package/.tfcore/utils/tf-mockup-parity.sh +14 -3
- package/.tfcore/utils/tf-owner-text.py +27 -5
- package/.tfcore/utils/tf-phase.sh +24 -4
- package/.tfcore/utils/tf-render-html.py +19 -3
- package/.tfcore/utils/tf-req-ids.py +36 -0
- package/.tfcore/utils/tf-stack-check.sh +3 -0
- package/.tfcore/utils/tf-status-facts.py +90 -15
- package/.tfcore/utils/tf-theme.mjs +189 -0
- package/.tfcore/utils/tf-triage.py +30 -4
- package/.tfcore/utils/tf-verify-boot.sh +296 -21
- package/.tfcore/utils/tf-verify-emit.sh +37 -17
- package/.tfcore/utils/tf-verify-list.py +219 -20
- 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 +184 -20
- package/.tfcore/utils/tf-verify-screens.sh +17 -4
- package/.tfcore/utils/tf-verify-tests.sh +107 -17
- package/.tfcore/utils/tf-verify-verdict.py +118 -15
- 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_feedback.py +86 -18
- package/.tfcore/utils/tf_stack_check.py +93 -0
- package/README.md +2 -2
- package/opencode.jsonc +6 -2
- package/package.json +1 -1
- package/scripts/install.mjs +14 -3
- 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,30 @@
|
|
|
1
|
+
# develop-end-to-end
|
|
2
|
+
|
|
3
|
+
`*develop-end-to-end {App} {brief}` takes a brief and develops a new app from it, unattended, to the point where the owner starts UAT: Day 1 (both stages), the build with its inline verify and fixes, and the handoff (UsageGuide, smoke checklist, test users). Greenfield only. No question, no confirmation, no review stop (`.tfcore/tasks/_develop-mode.md`). It commits and pushes after every phase and reports the time and tokens of each phase and of the whole build.
|
|
4
|
+
|
|
5
|
+
## Before it starts
|
|
6
|
+
|
|
7
|
+
The owner has made a git repository for the app with a remote (GitHub) and scaffolded TechieFlow into it. `{brief}` is a file path or the brief's text; text is written to `docs/{App}-Brief.md` as given, never shortened.
|
|
8
|
+
|
|
9
|
+
Refuse, with one line saying what is missing, when: the folder is not a git repository, it has no remote, it already has a `docs/{App}-BRD.md` or source code (this command is greenfield only: use `*build-phase`), or a develop run is already active (`.tfcore/.session/develop.json` exists: offer `--resume`).
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
First: `bash .tfcore/utils/tf-phase.sh start develop-end-to-end {App}` prints the start time; keep it.
|
|
14
|
+
|
|
15
|
+
1. Write the brief to `docs/{App}-Brief.md` when it was given as text. Then record this launch, before the run starts, so its record never overlaps the phases' own (`.tfcore/tasks/_metrics-emit-gate.md`): `tf-emit.sh runs` with `"cmd":"develop-end-to-end","mode":"launch"`, the start time and no rows, then `bash .tfcore/utils/tf-phase.sh end`. The phases record themselves; the Build Report adds them up.
|
|
16
|
+
2. Start the supervisor detached, so it outlives this session, with the harness this session runs in:
|
|
17
|
+
`bash .tfcore/utils/tf-develop.sh . --app {App} --brief docs/{App}-Brief.md --harness <claude|opencode> --detach`
|
|
18
|
+
(`--model <id>` when the owner named one; `--no-push` only when the owner said not to push.)
|
|
19
|
+
3. Wait until `.tfcore/.session/develop.log` shows `day1 started`, then say, in three lines: the run has started; it is followed in `.tfcore/.session/develop.log`; the result will be `docs/{App}-Build-Report.md`. End the turn. This session does none of the phases itself.
|
|
20
|
+
|
|
21
|
+
`*develop-end-to-end {App} --resume` continues a stopped run: `bash .tfcore/utils/tf-develop.sh . --resume --detach`. A phase already done is not run again.
|
|
22
|
+
|
|
23
|
+
## What the run leaves
|
|
24
|
+
|
|
25
|
+
- One commit per phase (`TechieFlow develop-end-to-end: <phase> done — {App}`), pushed, and a last one with the final report.
|
|
26
|
+
- `docs/{App}-Build-Report.md`: built in how long, how long the agent worked, the tokens, one row per phase.
|
|
27
|
+
- `docs/metrics/develop-report.json`: the same figures for a comparison harness, per phase and in total, by model.
|
|
28
|
+
- The usual run records, misses and PROJECT-STATUS, written by each phase's own commands.
|
|
29
|
+
|
|
30
|
+
A usage limit pauses the run until the limit resets, then it resumes (`tf-goal.sh`). A phase the agent declares blocked stops the run with the blocker in PROJECT-STATUS "Known blockers" and in the report; `--resume` continues once the owner has dealt with it.
|
|
@@ -11,5 +11,6 @@ First: `bash .tfcore/utils/tf-phase.sh start fix-issues {App}` prints the start
|
|
|
11
11
|
3. `bash .tfcore/utils/tf-build.sh` until PASS, then smoke every fixed screen with `tf-verify-screens.sh --screen …` (`.tfcore/tasks/_smoke-test-policy.md`); a screen that still fails re-enters step 2 for its rows. When the evidence was a failed CI run, `bash .tfcore/utils/tf-ci-repro.sh` must also print PASS; a local build with warm caches does not prove a CI fix. The report says the fix passed that reproduction and that the owner's next CI run is the final check, never that CI is fixed.
|
|
12
12
|
4. Execute `.tfcore/tasks/verify-phase.md` inline, scoped to the touched rows, with its own step 0; rows of another phase's checklist are a second verify with `--phase N`. Only that run writes `Verified`.
|
|
13
13
|
5. `bash .tfcore/utils/tf-verify-boot.sh stop`, then `bash .tfcore/utils/tf-fix-close.sh {App} --started <the step-0 time> --subagents <what was fanned out> --build <pass|fail>`: the fix-issues run record first, then one miss-fix per touched row with the verifier's verdict from the ledger. A row the verifier left below `Verified` stays open with that verdict on its miss-fix.
|
|
14
|
+
5a. On a library (`metrics.project_type: library` in `core-config.yaml`), the documents the package ships describe this fix before the run ends, so one release carries the code and its documents: `.tfcore/tasks/handoff-phase.md` steps 1, 3, 3a and 4 (the UsageGuide, the HTMLs, the DevGuide with `--update`, the feedback replies), and `docs/{App}-AI-Reference.md` updated for every component or option this run changed. No separate handoff run record; this run's covers it. Write the log row's Phase cell as `fix-issues + handoff` (or `triage-and-fix + handoff`), so the gate offers the release, not `*handoff-phase`. Never tell the owner to run the handoff after the release.
|
|
14
15
|
6. Run the status gate (`.tfcore/tasks/_status-update-gate.md`); the run record is the one step 5 wrote. When this run is the last step of a goal, write the sentinel (`.tfcore/tasks/_yolo-mode.md`).
|
|
15
16
|
7. Report: evidence read, issues by kind, rows fixed and now `Verified`, rows still open and why, library gaps logged, and the next command from the gate.
|
package/.tfcore/tasks/mockups.md
CHANGED
|
@@ -11,7 +11,7 @@ First: `bash .tfcore/utils/tf-phase.sh start mockups {App}` prints the start tim
|
|
|
11
11
|
3. Read the BRD's Screens and flow table: every row is a screen, with its route, roles and fields; a dialog row belongs to its parent screen. A screen that already has a mockup in `docs/mockups/` keeps it and is linked. An app with no screens records "skipped — no UI" in the UIDesign and stops.
|
|
12
12
|
4. For each screen without a mockup (fan out one sub-agent per group of screens when there are many): decide the layout regions, map every region to a control, write `docs/mockups/<screen-slug>.html` with realistic placeholder data in the library's visual language, and fill the screen's section in `docs/{App}-UIDesign.md` from `.tfcore/templates/v4custom/app-uidesign-tmpl.md` — for phase n of a Large project, `docs/{App}-Pn-UIDesign.md` from `app-phase-uidesign-tmpl.md`, which carries that phase's screens only (mockup link, roles, regions-to-controls table, fields table, the dialogs it opens, the empty, loading and error states). One shell, one theme, one spacing system across the set.
|
|
13
13
|
5. Make the set click through. The entry screen is the sign-in page (or onboarding, or the home page when there is neither). Every link and every navigation button goes to the mockup it names, written as `<a href="screen.html">` or a form action, never as a script (`onclick="location.href=…"` is refused: it dies in any viewer without scripts): sign in opens the home screen, logout returns to sign in, an "Add" button opens the add screen, a row opens its detail, every menu item leads to its screen. Every screen has a way out. A button that does not navigate shows a message with `onclick`. A dialog opens on its parent page. Sign-in needs no credentials: the button alone navigates.
|
|
14
|
-
6. Anchor every element the build must match with `data-testid`, using the names from the regions-to-controls table: the layout regions, every badge and status indicator, every icon button, every card, table and chart, and every value cell whose width matters. The verifier compares only anchored elements; a mockup without anchors is ungradeable.
|
|
14
|
+
6. Anchor every element the build must match with `data-testid`, using the names from the regions-to-controls table: the layout regions, every badge and status indicator, every icon button, every card, table and chart, and every value cell whose width matters. The verifier compares only anchored elements; a mockup without anchors is ungradeable. A box drawn only in another state (an error, a newer version) carries `data-tf-state="<state>"`; a row of sample data a fresh app has none of carries `data-tf-sample`. Draw every menu and drop-down open, its box marked `data-tf-state="popup-open"` inside the anchored control that opens it, each item that opens a screen an `<a href="<that mockup>.html">`, and sample choices (branch names, past chats) `data-tf-sample`: verify opens each, clicks every item, and fails a missing one.
|
|
15
15
|
7. `bash .tfcore/utils/tf-doc-check.sh --app {App}`: it refuses a broken link, an unreachable or dead-end screen, an inert button and an unanchored mockup. Fix every FAIL.
|
|
16
16
|
8. When run on its own (not inside a day-1 task), run the status gate (`.tfcore/tasks/_status-update-gate.md`) with `"cmd":"mockups"`.
|
|
17
17
|
9. Report: screens mocked, screens kept, controls used, library gaps logged, and the files written.
|
|
@@ -8,7 +8,7 @@ First: `bash .tfcore/utils/tf-phase.sh start triage-and-fix {App}` prints the st
|
|
|
8
8
|
|
|
9
9
|
1. Compare. `bash .tfcore/utils/tf-verify-list.sh {App} all`, `tf-verify-boot.sh start`, then `tf-verify-screens.sh --list … --base <url>` and `tf-mockup-parity.sh --base <url> --screen … --json-out tests/.artifacts/verify/parity.json` over every screen. Every screen that renders empty, looks broken or drifts from its mockup is an issue, added to the owner's evidence with its screenshot.
|
|
10
10
|
2. Triage. `.tfcore/tasks/triage-issues.md` steps 3 and 4 on the combined issue list, then `tf-triage.sh {App} close --started <the step-0 time> --cmd fix-issues`.
|
|
11
|
-
3. Fix. `.tfcore/tasks/fix-issues.md` steps 2 to 5 on the demoted rows (the builders, the build, the smoke, the verify, `tf-fix-close.sh`).
|
|
11
|
+
3. Fix. `.tfcore/tasks/fix-issues.md` steps 2 to 5 on the demoted rows (the builders, the build, the smoke, the verify, `tf-fix-close.sh`), with `--cmd triage-and-fix` on `tf-fix-close.sh`: it writes this run's record, filed under this command, around the verify it chained. On a library (`metrics.project_type: library`), `fix-issues.md` step 5a too: the shipped documents are brought up to date now, before the gate.
|
|
12
12
|
4. Metrics. Execute `.tfcore/tasks/metrics-report.md`.
|
|
13
|
-
5. Run the status gate (`.tfcore/tasks/_status-update-gate.md`)
|
|
13
|
+
5. Run the status gate (`.tfcore/tasks/_status-update-gate.md`); the run record is the one step 3 wrote, so do not emit another (one over the same window is refused). When this run is the last step of a goal, write the sentinel (`.tfcore/tasks/_yolo-mode.md`).
|
|
14
14
|
6. Summary, one short section per step: screens compared and what the comparison found; root causes found and logged, each with its miss id and discovery cost; fixes made and logged, each with its verdict and fix cost; rows re-verified by status; metrics refreshed, the report's path; the next command from the gate.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# verify-phase
|
|
2
2
|
|
|
3
|
-
`*verify {scope} {App}` proves every checklist row in scope against the running application and writes the verdicts. Scope is `ui`, `functional`, `all` or a list of ids; a caller that chains this task passes its scope and nothing is asked. YOLO is its default mode. The application runs on this machine, never on a cloud host. This task never edits source, the BRD or the mockups. It writes tests under `tests/verify/`, evidence under `tests/.artifacts/verify/`, the ledger `docs/.last-verify.json`, and the checklist cells through the verdict script only: a `Verified` written by hand is refused by the hook.
|
|
3
|
+
`*verify {scope} {App} [--phase N|all]` proves every checklist row in scope against the running application and writes the verdicts. Scope is `ui`, `functional`, `all` or a list of ids; a caller that chains this task passes its scope and nothing is asked. YOLO is its default mode. The application runs on this machine, never on a cloud host. This task never edits source, the BRD or the mockups. It writes tests under `tests/verify/`, evidence under `tests/.artifacts/verify/`, the ledger `docs/.last-verify.json`, and the checklist cells through the verdict script only: a `Verified` written by hand is refused by the hook.
|
|
4
4
|
|
|
5
5
|
First: `bash .tfcore/utils/tf-phase.sh start verify-phase {App}` prints the start time; keep it. Then `bash .tfcore/utils/tf-yolo.sh on --source verify` unless YOLO is already on.
|
|
6
6
|
|
|
@@ -10,11 +10,11 @@ Applied to every row in this order; the first that fails is the row's verdict.
|
|
|
10
10
|
|
|
11
11
|
| Check | Question | Tool |
|
|
12
12
|
|---|---|---|
|
|
13
|
-
| build | does the application build and boot | `tf-verify-boot.sh` |
|
|
13
|
+
| build | does the application build and boot, and does its code reference every package the Architecture's `Required packages:` line lists | `tf-verify-boot.sh`; the verdict runs `tf-stack-check.sh` itself |
|
|
14
14
|
| acceptance | does the test carrying the row's id pass | `tf-verify-tests.sh` |
|
|
15
15
|
| render | does every control the mockup anchors show something, no header-only table, no blank page, no error | `tf-verify-screens.sh` |
|
|
16
16
|
| assets | did every stylesheet and script the screen declares arrive | `tf-assets.sh` |
|
|
17
|
-
| visual | nothing overlaps, nothing clipped or off-screen, no sideways scroll, a stylesheet loaded | `tf-verify-screens.sh` |
|
|
17
|
+
| visual | nothing overlaps, nothing clipped or off-screen, no sideways scroll, a stylesheet loaded, text readable (3.0 : 1) in light and in dark mode, no light panel in dark mode | `tf-verify-screens.sh` |
|
|
18
18
|
| mockup | does the built screen carry the structure its mockup draws | `tf-mockup-parity.sh` |
|
|
19
19
|
| speed | within the row's `perf-budget:` line, only when the row declares one | `tf-perf.sh`, `tf-perf-grade.sh` |
|
|
20
20
|
|
|
@@ -22,22 +22,23 @@ Standards has no script yet and is never listed as run. A row is `Verified` only
|
|
|
22
22
|
|
|
23
23
|
## Read
|
|
24
24
|
|
|
25
|
-
The phase's checklist, UIDesign and `docs/mockups
|
|
25
|
+
The phase's checklist, UIDesign and `docs/mockups/`; the UsageGuide's test users. The phase is `--phase N` when given, else `appPhase` in `core-config.yaml`. `--phase all` grades every phase's checklist in turn, phase 1 first: `tf-verify-list.sh {App} {scope} --phase all` prints `PHASES 1 2 3`, then steps 1 and 4 to 6 run once per phase, each with `--phase <n>`, on one boot (steps 2, 3 and 7 once). Each pass writes only its own phase's checklist; the ledger and telemetry name the phase.
|
|
26
26
|
|
|
27
27
|
## Steps
|
|
28
28
|
|
|
29
|
-
1. `bash .tfcore/utils/tf-verify-list.sh {App} {scope}
|
|
29
|
+
1. `bash .tfcore/utils/tf-verify-list.sh {App} {scope} --phase <n>` prints the rows to grade with their acceptance line, screen, route, mockup and perf budget, the screens to drive, and the test users; it writes `tests/.artifacts/verify/list.json`. Every row in scope is graded whatever its status; only N/A is skipped, so a `Verified` row is re-confirmed and an old `Done` claim is tested. NOTHING means run the status gate and stop. A row without an acceptance line cannot be graded: fix the checklist first. A dialog is listed under its parent screen and checked there; never give a dialog a route so it can be tested.
|
|
30
30
|
2. `bash .tfcore/utils/tf-verify-env.sh` prints READY, or the one command the owner must run once.
|
|
31
|
-
3. `bash .tfcore/utils/tf-verify-boot.sh start` boots the head it finds (`--head` or `--project` when there are several) and prints BOOTED with a URL, or a CDP URL for an embedded-browser desktop head, which the browser tools attach to. A dependent service (database, API) is yours to start first, from its own configuration, in order. NONE `kind=build-error`: every row fails the build check; go to step 6. NONE `kind=host` or `kind=no-driver` (an Android
|
|
32
|
-
4. Tests. Every UI and functional row has one test whose title starts with its id, written from the acceptance line, black box, under `tests/verify/`; an NFR row without a screen has a unit test carrying its id in the test project, or is not observable. Reuse a test that exists. Fan out one test-writer sub-agent per screen group in one turn, each with its rows, their acceptance lines, the URL and the test user; a test writer touches no source. Sign in as a UsageGuide test user (`.tfcore/tasks/_smoke-test-policy.md`).
|
|
31
|
+
3. `bash .tfcore/utils/tf-verify-boot.sh start` boots the head it finds (`--head` or `--project` when there are several) and prints BOOTED with a URL, or a CDP URL for an embedded-browser desktop head, which the browser tools attach to. A dependent service (database, API) is yours to start first, from its own configuration, in order. NONE `kind=build-error`: every row fails the build check; go to step 6. NONE `kind=host` or `kind=no-driver` (an Android or iOS head): go to step 6 as well; those rows are written "not verified" with the reason. A Mac desktop head is booted on a Mac and prints `mode=appium` with the Appium address; when a project also has a web head the default stays `web`, so pass `--head maccatalyst` when the owner asked for the Mac head. A Mac head whose screens are drawn in a web view is checked without control names (blank, error bar, overlap, screenshot), and its rows say the names were not measured. Never a static-only pass, never "the owner should start it".
|
|
32
|
+
4. Tests. Every UI and functional row has one test whose title starts with its id, written from the acceptance line, black box, under `tests/verify/`; an NFR row without a screen has a unit test carrying its id in the test project, or is not observable. Reuse a test that exists. A screen whose mockup draws data a fresh app lacks gets `tests/verify/seed/<mockup name>.sh`, which puts the running app (`$TF_BASE`) in the drawn state; `tf-verify-screens.sh` runs it first. Fan out one test-writer sub-agent per screen group in one turn, each with its rows, their acceptance lines, the URL and the test user; a test writer touches no source. Sign in as a UsageGuide test user (`.tfcore/tasks/_smoke-test-policy.md`).
|
|
33
33
|
5. Run the checks, with `<url>` from step 3 and, on a signed-in app, the session cookie from the test login for the tools that take `--cookie`:
|
|
34
34
|
- `bash .tfcore/utils/tf-verify-tests.sh --base <url>`
|
|
35
|
-
- `bash .tfcore/utils/tf-verify-screens.sh --list tests/.artifacts/verify/list.json --base <url>` (or `--cdp <url>`; `--login-path /login --user … --password …` when the app has a sign-in page)
|
|
35
|
+
- `bash .tfcore/utils/tf-verify-screens.sh --list tests/.artifacts/verify/list.json --base <url>` (or `--cdp <url>`; or `--appium <url>` for a Mac desktop head, whose anchors are the controls' accessibility identifiers and whose mockup and asset checks do not run; `--login-path /login --user … --password …` when the app has a sign-in page)
|
|
36
36
|
- `bash .tfcore/utils/tf-assets.sh --base <url> --paths "<every screen route, comma-separated>" --json-out tests/.artifacts/verify/assets.json`
|
|
37
|
-
- `bash .tfcore/utils/tf-mockup-parity.sh --base <url> --screen <name>=<route> … --json-out tests/.artifacts/verify/parity.json` (
|
|
37
|
+
- `bash .tfcore/utils/tf-mockup-parity.sh --base <url> --screen <name>=<route> … --json-out tests/.artifacts/verify/parity.json` (the `Mockup parity:` line step 1 printed; a screen whose route takes a value is added with the value filled in, `--screen <mockup name>=/processes/6/run`; skip on a project with no mockups). Screens and parity both run in light and, when the app has one, dark mode (`--themes` forces it). A driven screen with a mockup and no comparison fails the mockup check
|
|
38
38
|
- for each row with a `perf-budget:` line: `bash .tfcore/utils/tf-perf.sh --base <url> --paths "<its screen's route>" --levels <its concurrency> --requests 24 --build-config <Release or Debug, as booted> --json-out tests/.artifacts/verify/perf/<REQ>.json`
|
|
39
|
+
- for the rows ending `zoom-check:` (a desktop head drawn in a web view; the stack's standard says how it zooms), on each booted head: `bash .tfcore/utils/tf-verify-zoom.sh --appium <url>` (or `--cdp <url>`) `--leave 3 --json-out tests/.artifacts/verify/zoom/<the keys row>.json`; stop and start the head; then the same with `--expect-restored 1.3 --json-out tests/.artifacts/verify/zoom/<the kept row>.json`
|
|
39
40
|
Open every failing screenshot under `tests/.artifacts/verify/screens/` and look at it before step 6.
|
|
40
|
-
6. `bash .tfcore/utils/tf-verify-verdict.sh {App} --apply --started <the step-0 time>` applies the seven checks in order, writes the ledger and rewrites the Status, % and Remarks cells of every graded row, and prints the verdict table. Then `bash .tfcore/utils/tf-verify-emit.sh {App} --started <the step-0 time>` appends the gate records, one miss per newly failing row, and this run's record.
|
|
41
|
+
6. `bash .tfcore/utils/tf-verify-verdict.sh {App} --apply --started <the step-0 time>` applies the seven checks in order, writes the ledger and rewrites the Status, % and Remarks cells of every graded row, and prints the verdict table. Both read the phase from `list.json`. Then `bash .tfcore/utils/tf-verify-emit.sh {App} --started <the step-0 time>` appends the gate records, one miss per newly failing row, and this run's record.
|
|
41
42
|
7. `bash .tfcore/utils/tf-verify-boot.sh stop`.
|
|
42
43
|
8. Run the status gate (`.tfcore/tasks/_status-update-gate.md`); its run record is the one step 6 wrote. A head that could not be driven is named under "Known blockers". When this run is the last step of a goal, write the sentinel (`.tfcore/tasks/_yolo-mode.md`). When the verifier was chained by a build or a fix, that caller runs the gate.
|
|
43
|
-
9. Report: what was booted (head, rung, URL); rows by verdict; the first-failing-check counts; screens driven and screenshots; the parity coverage line (screens graded, failed, ungradeable, without a mockup); the speed lines only for rows that carry a budget; every not-verified row with its reason; the evidence folder; the next command from the gate.
|
|
44
|
+
9. Report: what was booted (head, rung, URL); rows by verdict, per phase; the first-failing-check counts; screens driven and screenshots; the parity coverage line (screens graded, failed, ungradeable, without a mockup); the speed lines only for rows that carry a budget; every not-verified row with its reason; the evidence folder; the next command from the gate.
|
|
@@ -93,7 +93,7 @@ So `tf-emit.sh` detects it and injects it. **Never write `harness` into an emit
|
|
|
93
93
|
|
|
94
94
|
| Field | Type | Values / notes |
|
|
95
95
|
|---|---|---|
|
|
96
|
-
| `cmd` | string | `day1-brownfield` \| `day1-greenfield` \| `split-brd` \| `mockups` \| `build-phase` \| `verify-phase` \| `fix-issues` \| `triage-issues` \| `log-miss` \| `devguide` \| `productguide` \| `handoff-phase` \| `refresh-status` \| `amend-docs` \| `deploy-checklist` (added 2026-09-06) \| `metrics-report` \| `generate-html` \| `render-workflow-docs` \| `triage-and-fix` \| `framework-reset` (framework maintenance, D-19; all five added 2026-09-07, after FR-37's check was built and found 27 records already carrying a value the schema did not list). A value outside this list is a defect in the emitting task, not in the reader: the reader keeps it. |
|
|
96
|
+
| `cmd` | string | `day1-brownfield` \| `day1-greenfield` \| `split-brd` \| `mockups` \| `build-phase` \| `verify-phase` \| `fix-issues` \| `triage-issues` \| `log-miss` \| `devguide` \| `productguide` \| `handoff-phase` \| `refresh-status` \| `amend-docs` \| `deploy-checklist` (added 2026-09-06) \| `metrics-report` \| `generate-html` \| `render-workflow-docs` \| `triage-and-fix` \| `develop-end-to-end` (added 2026-10-08: only its launch; each phase records itself) \| `framework-reset` (framework maintenance, D-19; all five added 2026-09-07, after FR-37's check was built and found 27 records already carrying a value the schema did not list). A value outside this list is a defect in the emitting task, not in the reader: the reader keeps it. |
|
|
97
97
|
| `mode` | string \| null | `build` \| `fix`. `build-phase` already distinguishes these (FIX mode) — capture it; the ratio is the rework metric. `null` for commands with no mode. |
|
|
98
98
|
| `started` | string | ISO-8601 UTC. When the task began — the timestamp you noted at step 0, not "now minus a guess". |
|
|
99
99
|
| `ended` | string | ISO-8601 UTC. Normally equal to `ts`. |
|
|
@@ -105,6 +105,7 @@ So `tf-emit.sh` detects it and injects it. **Never write `harness` into an emit
|
|
|
105
105
|
| `build_result` | string \| null | `pass` \| `fail` \| `not-run`. |
|
|
106
106
|
| `harness` | string \| null | Injected by `tf-emit.sh` (§1). **Do not emit it yourself.** |
|
|
107
107
|
| `yolo` | bool | Whether YOLO / goal mode was on for this run (D-12, FR-35; added 2026-09-05). Written by the task from the flag file at the status gate; wired into every task in Session 4c. |
|
|
108
|
+
| `app_phase` | int | **`verify-phase` only, added 2026-10-11 (Sevak TF-005).** The phase whose checklist this pass graded: 1 is `docs/<App>-Checklist.md`, n is `docs/<App>-P<n>-Checklist.md`. Absent on older records, which graded `appPhase`. One `*verify --phase all` writes one run record per phase, each starting where the one before ended (§2.7b). |
|
|
108
109
|
|
|
109
110
|
---
|
|
110
111
|
|
|
@@ -236,6 +237,7 @@ Failure:
|
|
|
236
237
|
| `gates_run` | string[] | Which gates actually executed this pass — subset of `["build","acceptance","render","assets","visual","mockup-parity","perf","standards"]`, in execution order. A gate absent here did **not** run and cannot be credited or blamed. For `perf`, `assets` and `mockup-parity` this carries real weight: see §3.5. |
|
|
237
238
|
| `failure_class` | string \| null | Controlled vocabulary only, §3.3. `null` on a pass. |
|
|
238
239
|
| `prior_verdict` | string \| null | The Status cell value **before** this run wrote over it. `null` if the row is new. |
|
|
240
|
+
| `app_phase` | int | The phase whose checklist holds `req_id` and was written by this pass (added 2026-10-11, Sevak TF-005); see §2. Absent on older records. |
|
|
239
241
|
| `proof` | string \| null | Optional, §3.4. |
|
|
240
242
|
|
|
241
243
|
### 3.1 `attempt` — the field first-pass rate depends on entirely
|
|
@@ -281,7 +283,9 @@ Get this right; everything else on the record is secondary.
|
|
|
281
283
|
|
|
282
284
|
### 3.3 `failure_class` — controlled vocabulary, no free text
|
|
283
285
|
|
|
284
|
-
`blank-data` · `zero-rows` · `overlap` · `clipped` · `offscreen` · `slow-ttfb` · `slow-load` · `timeout` · `exception` · `assert-fail` · `naming` · `build-error` · `missing-asset` · `mockup-drift` · `other`
|
|
286
|
+
`blank-data` · `zero-rows` · `overlap` · `clipped` · `offscreen` · `low-contrast` · `light-surface` · `slow-ttfb` · `slow-load` · `timeout` · `exception` · `assert-fail` · `naming` · `build-error` · `missing-asset` · `mockup-drift` · `other`
|
|
287
|
+
|
|
288
|
+
`low-contrast` and `light-surface` (added 2026-10-07, Lekhak TF-024) are visual findings: text below the contrast floor against the colour painted behind it, in either theme, and a large light panel in dark mode. The ratio and the colours stay in the run JSON and the Remark.
|
|
285
289
|
|
|
286
290
|
`missing-asset` (added 2026-08-31) names an asset the page **declared** and never received — a 404, a non-200, or a 200 with an empty body on a `<link rel=stylesheet>` or `<script src>`. It says only that; **never** the URL, which is a file path inside the app and is exactly what constraint 7 keeps out of these streams. The path belongs in the checklist Remark a human reads.
|
|
287
291
|
|
|
@@ -1251,8 +1251,17 @@ def analyse(repos):
|
|
|
1251
1251
|
voided_runs = 0
|
|
1252
1252
|
void_reasons = []
|
|
1253
1253
|
void_orphans = 0
|
|
1254
|
+
gates_malformed = []
|
|
1254
1255
|
for repo in repos:
|
|
1255
1256
|
g = read_stream(repo, "gates")
|
|
1257
|
+
# A gate record is one REQ verdict (SCHEMA.md §3). A suite-level note with no req_id or no
|
|
1258
|
+
# verdict ({"gate":"verify-suite","result":"pass"}) was counted as a failure, passes
|
|
1259
|
+
# included (Lekhak TF-020). It is scored nowhere and listed as malformed instead.
|
|
1260
|
+
bad_g = [x for x in g if not x.get("req_id") or not x.get("verdict")]
|
|
1261
|
+
if bad_g:
|
|
1262
|
+
g = [x for x in g if x.get("req_id") and x.get("verdict")]
|
|
1263
|
+
gates_malformed += [{"app": app_name(repo), "ts": x.get("ts"), "gate": x.get("gate"),
|
|
1264
|
+
"result": x.get("result")} for x in bad_g]
|
|
1256
1265
|
r, vn, vr, vo = apply_voids(read_stream(repo, "runs"), app_name(repo))
|
|
1257
1266
|
voided_runs += vn; void_reasons += vr; void_orphans += vo
|
|
1258
1267
|
repo_misses = read_stream(repo, "misses")
|
|
@@ -1273,6 +1282,7 @@ def analyse(repos):
|
|
|
1273
1282
|
per_repo.append({"repo": repo, "app": app_name(repo),
|
|
1274
1283
|
"project_type": project_type(repo)[0],
|
|
1275
1284
|
"gates": len(g), "gates_backfilled": sum(1 for x in g if x.get("backfilled")),
|
|
1285
|
+
"gates_malformed": len(bad_g),
|
|
1276
1286
|
"runs": len(r), "sessions": len(s), "commits": len(c),
|
|
1277
1287
|
"misses": sum(1 for x in repo_misses if x.get("kind") == "miss" and x.get("miss_id") not in
|
|
1278
1288
|
{v.get("miss_id") for v in repo_misses if v.get("kind") == "miss-void"}),
|
|
@@ -1338,6 +1348,8 @@ def analyse(repos):
|
|
|
1338
1348
|
"runs_voided_n": voided_runs,
|
|
1339
1349
|
"runs_voided": void_reasons,
|
|
1340
1350
|
"run_voids_orphaned_n": void_orphans,
|
|
1351
|
+
"gates_malformed_n": len(gates_malformed),
|
|
1352
|
+
"gates_malformed": gates_malformed,
|
|
1341
1353
|
"misses": analyse_misses(misses),
|
|
1342
1354
|
"phases": analyse_phases(runs)}
|
|
1343
1355
|
|
|
@@ -1446,6 +1458,13 @@ def print_report(a, repos):
|
|
|
1446
1458
|
print(" Commit telemetry is not being written here. Fix it for good with")
|
|
1447
1459
|
print(" update-framework.sh <repo>, or reconcile what is already in the log:")
|
|
1448
1460
|
print(" .tfcore/telemetry/tf-metrics.sh --backfill-commits <repo>")
|
|
1461
|
+
if a.get("gates_malformed"):
|
|
1462
|
+
print("")
|
|
1463
|
+
print(" ⚠ %d gate record(s) carry no req_id or no verdict, so they are not one REQ's"
|
|
1464
|
+
% a["gates_malformed_n"])
|
|
1465
|
+
print(" verdict and are left out of every figure below (SCHEMA.md §3):")
|
|
1466
|
+
for x in a["gates_malformed"]:
|
|
1467
|
+
print(" %s %s gate=%s result=%s" % (x["app"], x["ts"], x["gate"], x["result"]))
|
|
1449
1468
|
print("")
|
|
1450
1469
|
|
|
1451
1470
|
for label in ("live", "backfilled"):
|
|
@@ -17,19 +17,23 @@
|
|
|
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 |
|
|
23
|
-
| Q8 | User interface | TrBlazeUI for every Blazor head and every MAUI Blazor Hybrid head. Native controls for WinForms, WPF and non-Blazor MAUI heads. Rendering mode is chosen per project and recorded. | Rendering mode only |
|
|
24
|
+
| Q8 | User interface | TrBlazeUI for every Blazor head and every MAUI Blazor Hybrid head: NuGet package `TrBlazeUI.Components` (it brings `TrBlazeUI.Primitives`), icons from `TrBlazeUI.Icons.Lucide`. Native controls for WinForms, WPF and non-Blazor MAUI heads. Rendering mode is chosen per project and recorded. | Rendering mode only |
|
|
24
25
|
| Q9 | Hosting | Web applications run containerised on a Bluehost VPS. The owner supplies a pipeline guidance document at UAT go-ahead, and a deployment checklist is produced from it. | Asked at UAT go-ahead, as the questionnaire specifies |
|
|
25
26
|
| Q10 | Production secrets | Decided when the owner asks for the production pipeline, after UAT. Until then no document states where production secrets live. | Asked after UAT |
|
|
26
27
|
| Q11 | Standing rules and prohibitions | 1. Never add a NuGet package without a line in the Architecture document saying why. 2. Never create a second configuration mechanism. 3. When TechieFlow, TrBlazeUI, TechieRag or any other internal library lacks something, record it in that library's feedback file and hold the feature until the library is fixed. Never implement a workaround. 4. Data access is always Dapper. 5. Database migrations live in a dedicated project named `<App>Db` (console or library) using the DbUp package. The web application runs the migration at startup, or the pipeline runs it after deployment. Never a `database` folder of loose scripts at the repository root. 6. Log files are written under the build output folder (`bin/`), which is git-ignored. Never at the repository root. 7. No unnecessary folders at the repository root (`data`, `database`, `scripts` and the like). Everything sits under `src/` or `tests/` according to what it belongs to. | No |
|
|
27
28
|
|
|
29
|
+
Required packages: `TrBlazeUI.Components` — for a project with a Blazor or MAUI Blazor Hybrid head. The Architecture copies this line into its Stack decisions for each package that applies, and `tf-stack-check.sh` fails the build check when no project file references one of them.
|
|
30
|
+
|
|
28
31
|
---
|
|
29
32
|
|
|
30
33
|
## Notes
|
|
31
34
|
|
|
32
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).
|
|
33
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.
|
|
34
38
|
- Q11.5 replaces the `database` folder of loose migration scripts that TfLens accumulated.
|
|
35
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.
|
|
@@ -22,6 +22,9 @@ rule: decisions-log
|
|
|
22
22
|
<!-- Authoring notes (agent only; never visible text).
|
|
23
23
|
Sections and order are fixed by the schema above. The Stack decisions table answers the stack
|
|
24
24
|
questions (.tfcore/templates/stack-questions.md); answers taken from an answer set cite it.
|
|
25
|
+
The "Required packages:" line copies the answer set's own line, keeping only the packages that
|
|
26
|
+
apply to this project's heads; tf-stack-check.sh fails the build check when the code references
|
|
27
|
+
none of a listed package (a model once replaced the mandated UI library with plain components).
|
|
25
28
|
No Deployment section: hosting is decided after UAT and lives in the Deployment Checklist; how a
|
|
26
29
|
developer runs the app lives in the UsageGuide. Detailed per-screen flows live in the DevGuide.
|
|
27
30
|
A brownfield project describes the code as it is; a planned change is a Decisions log row with
|
|
@@ -54,6 +57,8 @@ One row per stack question. "Source" says where the answer came from: the answer
|
|
|
54
57
|
| Q8 | User interface | {…} | {…} |
|
|
55
58
|
| Q11 | Standing rules | {…} | {…} |
|
|
56
59
|
|
|
60
|
+
Required packages: {each package the answer set requires for this project's heads, in backticks, or "none"}
|
|
61
|
+
|
|
57
62
|
## 2. Solution structure
|
|
58
63
|
|
|
59
64
|
| Project | Kind | Purpose |
|
|
@@ -94,8 +94,9 @@ One item per thing the verifier will test. Each names its screen, links the mock
|
|
|
94
94
|
| BRD-{N} | Performance | {…} | perf-budget: p95 load <= 2000ms @ concurrency 1 |
|
|
95
95
|
| BRD-{N} | Security | {…} | {…} |
|
|
96
96
|
| BRD-{N} | Logging | {from the Stack answer set} | {…} |
|
|
97
|
+
| BRD-{N} | Zoom (a desktop head drawn in a web view) | Cmd/Ctrl+=, - and 0 zoom 50% to 300% in 10% steps; the level is kept across restarts | zoom-check: keys · zoom-check: kept |
|
|
97
98
|
|
|
98
|
-
The `perf-budget:` measure is machine-read by the verifier, in exactly this form: `perf-budget: <p50|p95|max> <ttfb|load> <= <N>ms [@ concurrency <N>]`. Write one only where the owner stated a number.
|
|
99
|
+
The `perf-budget:` measure is machine-read by the verifier, in exactly this form: `perf-budget: <p50|p95|max> <ttfb|load> <= <N>ms [@ concurrency <N>]`. Write one only where the owner stated a number. The zoom row is for an app whose desktop head draws its screens in a web view (the stack's coding standards say how), and is deleted otherwise; its two checklist rows end `zoom-check: keys` and `zoom-check: kept`, which the verifier grades with `tf-verify-zoom.sh`.
|
|
99
100
|
|
|
100
101
|
## 7. Development status
|
|
101
102
|
|
|
@@ -45,6 +45,8 @@ rule: checklist-rows
|
|
|
45
45
|
| REQ-UI-001 | {short name} | Not Started | 0% | — | [view](#d-req-ui-001) |
|
|
46
46
|
| REQ-FN-001 | {short name} | Not Started | 0% | — | [view](#d-req-fn-001) |
|
|
47
47
|
| REQ-NFR-001 | {short name} | Not Started | 0% | — | [view](#d-req-nfr-001) |
|
|
48
|
+
| REQ-NFR-002 | Zoom with the OS keys (web-view desktop head only) | Not Started | 0% | — | [view](#d-req-nfr-002) |
|
|
49
|
+
| REQ-NFR-003 | Zoom level kept (web-view desktop head only) | Not Started | 0% | — | [view](#d-req-nfr-003) |
|
|
48
50
|
|
|
49
51
|
**Status values:** `Not Started` · `In Progress` · `Implemented` · `Verified` · `Done (pre-existing)` · `Needs re-verify` · `PARTIAL` · `FAIL` · `Blocked` · `Owner-UAT` · `N/A`. `Owner-UAT` marks a row only the owner can close, by following the UsageGuide test plan; the verifier cannot reach it from this machine.
|
|
50
52
|
|
|
@@ -63,3 +65,12 @@ rule: checklist-rows
|
|
|
63
65
|
<a id="d-req-nfr-001"></a>
|
|
64
66
|
- **REQ-NFR-001** — {the requirement}. *BRD:* BRD-{N}
|
|
65
67
|
- *Acceptance:* When {the measurement is taken}, then {the value within its limit}; perf-budget: p95 load <= 2000ms @ concurrency 1
|
|
68
|
+
|
|
69
|
+
<!-- A desktop head drawn in a web view carries these two rows (the stack's coding standards say how it zooms); delete them otherwise. -->
|
|
70
|
+
<a id="d-req-nfr-002"></a>
|
|
71
|
+
- **REQ-NFR-002** — Zoom with the OS keys. *BRD:* BRD-{N}
|
|
72
|
+
- *Acceptance:* When a user presses Cmd+= (Mac) or Ctrl+= (Windows) three times on {first screen}, then the page is drawn at 130%; zoom-check: keys
|
|
73
|
+
|
|
74
|
+
<a id="d-req-nfr-003"></a>
|
|
75
|
+
- **REQ-NFR-003** — The zoom level is kept. *BRD:* BRD-{N}
|
|
76
|
+
- *Acceptance:* When a user zooms to 130% on {first screen} and restarts the app, then it opens at 130%; zoom-check: kept
|
|
@@ -13,7 +13,8 @@ rule: phases-table
|
|
|
13
13
|
phases and are never reused, so the ranges never overlap. An item added to an earlier phase after a
|
|
14
14
|
later phase exists takes the next free id and the row gets a second range: "BRD-1 to BRD-73, BRD-111 to BRD-118". Every screen sits in exactly one phase;
|
|
15
15
|
the screens of a phase are exactly the "### Screen:" entries of that phase's UIDesign.
|
|
16
|
-
Status is planned, building or
|
|
16
|
+
Status is planned, building, done or kept-whole. kept-whole is only for a phase finished before the
|
|
17
|
+
size cap that the owner chose to keep as one document: its cap and word maximum then WARN, not FAIL. The phase being worked is appPhase in core-config.yaml
|
|
17
18
|
(bash .tfcore/utils/tf-day1-files.sh {App} --phase N); handoff moves it on.
|
|
18
19
|
tf-doc-check.sh --app {App} refuses a screen in two phases, an id in two phases, a BRD id outside
|
|
19
20
|
its phase's range, and a phase row without its BRD file. -->
|
|
@@ -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.
|
|
@@ -48,6 +48,8 @@ The same slug function MUST produce the `id="..."` on each heading AND the `href
|
|
|
48
48
|
|
|
49
49
|
The MD inline TOC and the HTML sidebar TOC use the SAME slug. The heading `id` uses the SAME slug. No exceptions.
|
|
50
50
|
|
|
51
|
+
**A second id, GitHub's.** A markdown table of contents written by hand links to the id GitHub gives the heading: digits kept, punctuation dropped, spaces to hyphens, a repeat suffixed `-1`, `-2` (`### 3.7 Group Service (GroupSvc)` → `#37-group-service-groupsvc`). When that id differs from the slug above, the heading also carries `<a id="{github-id}"></a>`, so those links land too (AppManager TF-029). A heading with no slug (H1, H5, H6) carries the GitHub id as its own.
|
|
52
|
+
|
|
51
53
|
---
|
|
52
54
|
|
|
53
55
|
## 2. CSS (inline at top of `<head>`, verbatim)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// tf-appium.mjs — the few WebDriver calls a native head needs, over Node's own fetch (no package).
|
|
2
|
+
// Used by tf-verify-native.mjs; an acceptance test of a native head may import it too:
|
|
3
|
+
// import { openSession, parseSource } from '<repo>/.tfcore/utils/tf-appium.mjs';
|
|
4
|
+
// const s = await openSession(process.env.APPIUM_URL, { bundleId: process.env.TF_BUNDLE_ID, appPath: process.env.TF_APP_PATH });
|
|
5
|
+
// await s.click(await s.find('accessibility id', 'save')); … await s.close();
|
|
6
|
+
// Every control is found inside the app under test by its AutomationId (the accessibility
|
|
7
|
+
// identifier); nothing here types or clicks at a screen position. Type into a field with
|
|
8
|
+
// `await s.type(el, 'text')`, the one way a Blazor Hybrid form on a Mac took the text.
|
|
9
|
+
|
|
10
|
+
import { execFileSync } from 'node:child_process';
|
|
11
|
+
import { statSync } from 'node:fs';
|
|
12
|
+
|
|
13
|
+
const W3C_EL = 'element-6066-11e4-a52e-4f735466cecf';
|
|
14
|
+
|
|
15
|
+
async function call(base, method, path, body) {
|
|
16
|
+
const r = await fetch(base + path, {
|
|
17
|
+
method, headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
19
|
+
signal: AbortSignal.timeout(300000),
|
|
20
|
+
});
|
|
21
|
+
const j = await r.json().catch(() => ({ value: { message: `HTTP ${r.status}, not JSON` } }));
|
|
22
|
+
if (!r.ok || (j.value && j.value.error)) throw new Error((j.value && j.value.message || `HTTP ${r.status}`).split('\n')[0]);
|
|
23
|
+
return j.value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// A session on the Mac Catalyst app (mac2), by its .app path when known: by bundle id alone mac2
|
|
27
|
+
// answered "could not be found" for a build outside /Applications that macOS had not registered
|
|
28
|
+
// (Lekhak TF-003; seen again 2026-10-07). skipAppKill: the app is tf-verify-boot.sh's to stop, so
|
|
29
|
+
// ending the session leaves it running for the next check.
|
|
30
|
+
export async function openSession(url, { bundleId, appPath = '', automationName = 'Mac2', platformName = 'mac', extra = {} } = {}) {
|
|
31
|
+
const base = url.replace(/\/$/, '');
|
|
32
|
+
const which = appPath ? { 'appium:appPath': appPath } : { 'appium:bundleId': bundleId };
|
|
33
|
+
const v = await call(base, 'POST', '/session', { capabilities: { alwaysMatch: {
|
|
34
|
+
platformName, 'appium:automationName': automationName, ...which,
|
|
35
|
+
'appium:skipAppKill': true, 'appium:newCommandTimeout': 300, ...extra } } });
|
|
36
|
+
const sp = `/session/${v.sessionId}`;
|
|
37
|
+
const s = {
|
|
38
|
+
id: v.sessionId,
|
|
39
|
+
call: (m, p, b) => call(base, m, sp + p, b),
|
|
40
|
+
source: () => s.call('GET', '/source'),
|
|
41
|
+
find: async (using, value, from = null) => Object.values(await s.call('POST', from ? `/element/${from}/element` : '/element', { using, value }))[0],
|
|
42
|
+
findAll: async (using, value, from = null) => (await s.call('POST', from ? `/element/${from}/elements` : '/elements', { using, value })).map((e) => e[W3C_EL] || Object.values(e)[0]),
|
|
43
|
+
click: (el) => s.call('POST', `/element/${el}/click`, {}),
|
|
44
|
+
// Typing into a field. A Blazor Hybrid form on a Mac kept its @bind empty or half filled when the
|
|
45
|
+
// value was set through Accessibility, through mac2's "set value", or by key events posted to the
|
|
46
|
+
// process ("The text must have valid value", Lekhak TF-026). What reached it: a click on the field,
|
|
47
|
+
// then W3C key actions, a key down and up per character.
|
|
48
|
+
type: async (el, text) => {
|
|
49
|
+
await s.click(el);
|
|
50
|
+
const actions = [...String(text)].flatMap((c) => [{ type: 'keyDown', value: c }, { type: 'keyUp', value: c }]);
|
|
51
|
+
await s.call('POST', '/actions', { actions: [{ type: 'key', id: 'keyboard', actions }] });
|
|
52
|
+
await s.call('DELETE', '/actions').catch(() => {});
|
|
53
|
+
},
|
|
54
|
+
shot: (el) => s.call('GET', el ? `/element/${el}/screenshot` : '/screenshot'),
|
|
55
|
+
close: () => call(base, 'DELETE', sp).catch(() => {}),
|
|
56
|
+
};
|
|
57
|
+
return s;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// What the app shows first, for tf-verify-boot.sh: BOOTED only when a mac2 session opens and the app
|
|
61
|
+
// has a window of its own that is not a system dialog (Lekhak TF-026: BOOTED was printed while no
|
|
62
|
+
// session could open, and once while the app's only window was macOS's "quit unexpectedly while
|
|
63
|
+
// reopening windows" dialog). That dialog is answered Don't Reopen, which only skips restoring old
|
|
64
|
+
// windows, and the answer is reported. The window alone is saved to `shot`. Never throws.
|
|
65
|
+
export async function firstScreen(url, { bundleId = '', appPath = '', shot = '', wait = 30000 } = {}) {
|
|
66
|
+
let s;
|
|
67
|
+
try { s = await openSession(url, { bundleId, appPath }); }
|
|
68
|
+
catch (e) { return { ok: false, stage: 'session', reason: e.message }; }
|
|
69
|
+
const sleep = (ms) => new Promise((ok) => setTimeout(ok, ms));
|
|
70
|
+
const DIALOG = new Set(['Dialog', 'Sheet', 'SystemDialog']);
|
|
71
|
+
const LEAF = new Set(['Button', 'TextField', 'SecureTextField', 'TextView', 'Image', 'CheckBox', 'Switch', 'Slider', 'PopUpButton',
|
|
72
|
+
'ComboBox', 'Link', 'RadioButton', 'SegmentedControl', 'Stepper', 'DatePicker', 'ProgressIndicator', 'Cell']);
|
|
73
|
+
const out = { ok: false, stage: 'window', dismissed: '' };
|
|
74
|
+
try {
|
|
75
|
+
for (const t0 = Date.now(); Date.now() - t0 < wait; await sleep(500)) {
|
|
76
|
+
const nodes = parseSource(await s.source());
|
|
77
|
+
const label = (n) => n.title || n.label || '';
|
|
78
|
+
const reopen = nodes.find((n) => n.type === 'Button' && /^don.t reopen$/i.test(label(n)));
|
|
79
|
+
if (reopen && !out.dismissed) {
|
|
80
|
+
const el = await s.find('-ios predicate string', `elementType == 9 AND (title BEGINSWITH[c] 'Don' OR label BEGINSWITH[c] 'Don')`).catch(() => null);
|
|
81
|
+
if (el) { await s.click(el); out.dismissed = 'macOS asked to reopen the windows of a run that did not quit; answered Don\'t Reopen'; continue; }
|
|
82
|
+
}
|
|
83
|
+
const dialog = nodes.find((n) => DIALOG.has(n.type)) || reopen;
|
|
84
|
+
const win = nodes.find((n) => n.type === 'Window' && n.w >= 200 && n.h >= 150 && !descendants(nodes, n.i).some((d) => DIALOG.has(d.type)));
|
|
85
|
+
// drawn: text or a control in the window's content, as tf-verify-native counts it: what sits
|
|
86
|
+
// directly on the window (its title, its three buttons) and the bars are not content. A web view
|
|
87
|
+
// took about eight seconds to draw its first page; until then the window is blank.
|
|
88
|
+
const chrome = (n) => n.parent === win.i || ['TabBar', 'Toolbar', 'NavigationBar', 'MenuBar', 'Menu'].some((t) => {
|
|
89
|
+
for (let p = n.i; p >= 0; p = nodes[p].parent) if (nodes[p].type === t) return true;
|
|
90
|
+
return false; });
|
|
91
|
+
const drawn = win && descendants(nodes, win.i).some((n) => !chrome(n) && (n.type === 'StaticText' ? !!label(n).trim() || !!(n.value || '').trim() : LEAF.has(n.type)));
|
|
92
|
+
if (win && !dialog && drawn) {
|
|
93
|
+
out.ok = true; out.window = { w: win.w, h: win.h };
|
|
94
|
+
if (shot) {
|
|
95
|
+
await sleep(500); // the first frame of a web view fades in
|
|
96
|
+
// the window alone (TF-027: mac2's element shot is a crop of the display, banners included)
|
|
97
|
+
if (appPath && windowShot(appPid(appPath), shot)) out.shot = shot;
|
|
98
|
+
else {
|
|
99
|
+
const el = await s.find('-ios predicate string', 'elementType == 4').catch(() => null);
|
|
100
|
+
if (el) { const { writeFileSync } = await import('node:fs'); writeFileSync(shot, Buffer.from(await s.shot(el), 'base64')); out.shot = shot; }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
out.reason = dialog ? `the app shows a dialog, not its first screen: "${(label(dialog) || dialog.type).slice(0, 80)}"`
|
|
106
|
+
: win ? 'the app\'s window is on view but blank (no text, no control)' : 'the app has no window of its own on view';
|
|
107
|
+
}
|
|
108
|
+
out.reason = `${out.reason || 'no window'} after ${Math.round(wait / 1000)} s`;
|
|
109
|
+
return out;
|
|
110
|
+
} catch (e) { return { ...out, reason: `Appium: ${e.message}` }; }
|
|
111
|
+
finally { await s.close(); }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The page source (XCUIElementType… XML) as a flat list of nodes with their parent index, so a
|
|
115
|
+
// caller can walk a subtree without an XML package.
|
|
116
|
+
export function parseSource(xml) {
|
|
117
|
+
const un = (s) => s.replace(/"/g, '"').replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>').replace(/ /g, '\n').replace(/&/g, '&');
|
|
118
|
+
const nodes = []; const stack = [];
|
|
119
|
+
const re = /<(\/?)(XCUIElementType\w+)([^>]*?)(\/?)>/g; let m;
|
|
120
|
+
while ((m = re.exec(xml))) {
|
|
121
|
+
if (m[1]) { stack.pop(); continue; }
|
|
122
|
+
const a = {}; let am; const are = /(\w+)="([^"]*)"/g;
|
|
123
|
+
while ((am = are.exec(m[3]))) a[am[1]] = un(am[2]);
|
|
124
|
+
const n = { i: nodes.length, type: m[2].replace('XCUIElementType', ''), id: a.identifier || '', label: a.label || '', value: a.value || '',
|
|
125
|
+
title: a.title || '', x: +a.x || 0, y: +a.y || 0, w: +a.width || 0, h: +a.height || 0, parent: stack.length ? stack[stack.length - 1] : -1, kids: [] };
|
|
126
|
+
if (n.parent >= 0) nodes[n.parent].kids.push(n.i);
|
|
127
|
+
nodes.push(n);
|
|
128
|
+
if (!m[4]) stack.push(n.i);
|
|
129
|
+
}
|
|
130
|
+
return nodes;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function descendants(nodes, i) {
|
|
134
|
+
const out = []; const todo = [...nodes[i].kids];
|
|
135
|
+
while (todo.length) { const k = todo.shift(); out.push(nodes[k]); todo.push(...nodes[k].kids); }
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- the app's own window, through macOS rather than mac2 (Lekhak TF-027) ---------------------------
|
|
140
|
+
// mac2 cannot resize a window, and its element screenshot is a crop of the display, so a notification
|
|
141
|
+
// banner over the window landed in every picture. macOS itself does both: System Events resizes the
|
|
142
|
+
// window (the app's minimum size still holds, and the size it really took is returned), and
|
|
143
|
+
// screencapture -l takes the window alone by its window number.
|
|
144
|
+
const run = (cmd, args) => { try { return execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 20000 }).trim(); } catch { return ''; } };
|
|
145
|
+
|
|
146
|
+
// the pid of the app running from this .app (a mac2 session starts it again under a new pid)
|
|
147
|
+
export function appPid(appPath) {
|
|
148
|
+
const out = run('pgrep', ['-f', `${appPath.replace(/\/$/, '')}/Contents/MacOS/`]);
|
|
149
|
+
return parseInt(out.split('\n')[0], 10) || 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// { id, x, y, w, h } of the app's largest normal window on view, or null
|
|
153
|
+
export function macWindow(pid) {
|
|
154
|
+
if (!pid) return null;
|
|
155
|
+
const js = `ObjC.import('CoreGraphics');
|
|
156
|
+
const l = ObjC.deepUnwrap(ObjC.castRefToObject($.CGWindowListCopyWindowInfo($.kCGWindowListOptionOnScreenOnly | $.kCGWindowListExcludeDesktopElements, 0)));
|
|
157
|
+
JSON.stringify(l.filter((w) => w.kCGWindowOwnerPID == ${pid} && w.kCGWindowLayer == 0)
|
|
158
|
+
.map((w) => ({ id: w.kCGWindowNumber, x: w.kCGWindowBounds.X, y: w.kCGWindowBounds.Y, w: w.kCGWindowBounds.Width, h: w.kCGWindowBounds.Height })))`;
|
|
159
|
+
try { return JSON.parse(run('osascript', ['-l', 'JavaScript', '-e', js]) || '[]').sort((a, b) => b.w * b.h - a.w * a.h)[0] || null; }
|
|
160
|
+
catch { return null; }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// sets the front window's size; returns the size it took ({ w, h }), or null when macOS refused
|
|
164
|
+
// (the terminal needs Accessibility, as mac2 does)
|
|
165
|
+
export function resizeWindow(pid, w, h) {
|
|
166
|
+
if (!pid) return null;
|
|
167
|
+
const tell = `tell application "System Events" to tell (first process whose unix id is ${pid})`;
|
|
168
|
+
run('osascript', ['-e', `${tell} to set size of window 1 to {${w}, ${h}}`]);
|
|
169
|
+
const got = run('osascript', ['-e', `${tell} to get size of window 1`]).split(/,\s*/).map(Number);
|
|
170
|
+
return got.length === 2 && got.every((n) => n > 0) ? { w: got[0], h: got[1] } : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// the window alone, by its window number; true when the file was written
|
|
174
|
+
export function windowShot(pid, file) {
|
|
175
|
+
const win = macWindow(pid);
|
|
176
|
+
if (!win) return false;
|
|
177
|
+
run('screencapture', ['-x', '-o', '-l', String(win.id), file]);
|
|
178
|
+
try { return statSync(file).size > 0; } catch { return false; }
|
|
179
|
+
}
|
|
@@ -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")
|