@techierathore/techieflow 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/TechieFlow/agents/flow-master.md +2 -0
- package/.claude/commands/TechieFlow/agents/verifier.md +2 -2
- package/.claude/commands/TechieFlow/tasks/_develop-mode.md +34 -0
- package/.claude/commands/TechieFlow/tasks/_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/guard-db.sh +106 -9
- 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/_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 +3 -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/html-render-shell.md +2 -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-build-list.py +16 -1
- package/.tfcore/utils/tf-build.sh +75 -0
- package/.tfcore/utils/tf-checklist-edit.py +21 -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 +83 -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 +355 -27
- package/.tfcore/utils/tf-mockup-parity.sh +14 -3
- 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 +12 -1
- package/.tfcore/utils/tf-verify-boot.sh +274 -19
- 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 +178 -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 +79 -16
- 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 +13 -2
- package/.tfcore/utils/__pycache__/tf-brd-status.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-bind.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-codex-telemetry.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-day1-files.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-log-miss.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-misses-md.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-mockups-locate.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-opencode-audit.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-render-html.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-triage.cpython-312.pyc +0 -0
- package/.tfcore/utils/__pycache__/tf-verify-verdict.cpython-312.pyc +0 -0
|
@@ -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"):
|
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
| Q5 | Logging | Serilog, always, with file logging wired at startup before anything else can fail. | No |
|
|
21
21
|
| Q6 | Tests | xUnit, always. A test project exists from day-1. | No |
|
|
22
22
|
| 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 |
|
|
23
|
+
| 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
24
|
| 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
25
|
| 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
26
|
| 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
27
|
|
|
28
|
+
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.
|
|
29
|
+
|
|
28
30
|
---
|
|
29
31
|
|
|
30
32
|
## Notes
|
|
@@ -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. -->
|
|
@@ -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
|
|
@@ -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
|
+
}
|
|
@@ -211,11 +211,26 @@ def main(argv):
|
|
|
211
211
|
if any(r["id"].upper().startswith("REQ-UI-") for r in work):
|
|
212
212
|
builder["UI"], why = ui_builder(app)
|
|
213
213
|
print(f"UI rows go to: {builder['UI']} — {why}")
|
|
214
|
+
# A defect on a screen goes to one builder. Split by builder, the UI and the backend clusters of one
|
|
215
|
+
# page both carried its mockup defect, both edited the same files at once, and one left a component
|
|
216
|
+
# half-written that broke every other builder's boot for an hour (Chatur TF-004). Such a page is
|
|
217
|
+
# one cluster, built by the UI builder when one of its UI rows carries the defect.
|
|
218
|
+
cls_of = {r["id"]: re.match(r"REQ-(UI|FN|RAG|NFR)-", r["id"]).group(1) for r in work}
|
|
219
|
+
sec_of = {r["id"]: ent.get(r["id"].upper(), ("Other", "", "", ""))[0] for r in work}
|
|
220
|
+
fixed = {}
|
|
221
|
+
for r in work:
|
|
222
|
+
s, cls = sec_of[r["id"]], cls_of[r["id"]]
|
|
223
|
+
if r["defects"] and cls in ("UI", "FN") and (cls == "UI" or s not in fixed):
|
|
224
|
+
fixed[s] = builder[cls]
|
|
214
225
|
for r in work:
|
|
215
226
|
sec, acc, mock, brd = ent.get(r["id"].upper(), ("Other", "", "", ""))
|
|
216
227
|
cls = re.match(r"REQ-(UI|FN|RAG|NFR)-", r["id"]).group(1)
|
|
217
|
-
|
|
228
|
+
who = fixed[sec] if cls in ("UI", "FN") and sec in fixed else builder[cls]
|
|
229
|
+
key = ("Non-functional" if cls == "NFR" else "RAG" if cls == "RAG" else sec, who)
|
|
218
230
|
clusters.setdefault(key, []).append((r, acc, mock, brd))
|
|
231
|
+
for s in sorted(fixed):
|
|
232
|
+
if builder["UI"] != builder["FN"] and {"UI", "FN"} <= {cls_of[r["id"]] for r in work if sec_of[r["id"]] == s}:
|
|
233
|
+
print(f"One builder for {s}: it carries a defect, so its UI and backend rows go to {fixed[s]} together")
|
|
219
234
|
print()
|
|
220
235
|
print("## Clusters (one sub-agent each; spawn them all in one turn)")
|
|
221
236
|
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
@@ -108,6 +108,47 @@ if [[ "$MODE" == "probe" ]]; then
|
|
|
108
108
|
fi
|
|
109
109
|
[[ -z "$TARGET" && "$MODE" != "run" ]] && { echo "NOT-RUN no .sln, .slnx or .csproj here; name the target"; exit 2; }
|
|
110
110
|
|
|
111
|
+
# ---- test on a solution: one test project at a time (TechieRag TF-004) ----------------------------
|
|
112
|
+
# `dotnet test <solution>` runs every test project at once. On a 7.8 GB WSL machine TechieRag's local-
|
|
113
|
+
# model tests took the memory while TechieRag.Tests walked folders, and a test that passes alone failed
|
|
114
|
+
# with "Cannot allocate memory", twice in one day. Each test project now runs on its own, through this
|
|
115
|
+
# same script, and the verdicts are merged into one line naming one joined log: FAIL if any failed
|
|
116
|
+
# (naming which), else NOT-RUN if any could not run, else PASS. TF_TEST_TOGETHER=1 runs them together.
|
|
117
|
+
if [[ "$MODE" == "test" && "${TF_TEST_TOGETHER:-0}" != "1" && "$TARGET" == *.sln* ]]; then
|
|
118
|
+
tests=()
|
|
119
|
+
for p in ${projects[@]+"${projects[@]}"}; do
|
|
120
|
+
f="$p"; [[ -f "$f" ]] || f="$base/$p"
|
|
121
|
+
[[ -f "$f" ]] || continue
|
|
122
|
+
grep -qiE 'Microsoft\.NET\.Test\.Sdk|<IsTestProject>[[:space:]]*true|"(xunit|xunit\.v3|NUnit|MSTest(\.TestFramework)?|TUnit)"|Include="(xunit|xunit\.v3|NUnit|MSTest|TUnit)' "$f" && tests+=("$f")
|
|
123
|
+
done
|
|
124
|
+
if [[ ${#tests[@]} -gt 1 ]]; then
|
|
125
|
+
if [[ $PRINT -eq 1 ]]; then
|
|
126
|
+
for t in "${tests[@]}"; do echo "dotnet test $t ${EXTRA[*]:-}"; done; exit 0
|
|
127
|
+
fi
|
|
128
|
+
mkdir -p tests/.artifacts/build
|
|
129
|
+
JOINED="tests/.artifacts/build/$(date -u +%Y%m%dT%H%M%SZ)-test-split-$$.log"; : > "$JOINED"
|
|
130
|
+
fails=(); notrun=(); passed=0; lines=(); pass=()
|
|
131
|
+
[[ ${#EXTRA[@]} -gt 0 ]] && pass=(-- "${EXTRA[@]}")
|
|
132
|
+
for t in "${tests[@]}"; do
|
|
133
|
+
out="$(TF_TEST_TOGETHER=1 bash "${BASH_SOURCE[0]}" test "$t" ${pass[@]+"${pass[@]}"} 2>&1)"
|
|
134
|
+
v="$(grep -m1 -E '^(PASS|FAIL|NOT-RUN)' <<<"$out")"
|
|
135
|
+
lg="$(sed -n 's/.*log \(tests\/\.artifacts\/build\/[^ ;]*\).*/\1/p' <<<"$v" | head -1)"
|
|
136
|
+
{ echo "### test project: $t"; [[ -n "$lg" && -f "$lg" ]] && cat "$lg" || echo "$out"; } >> "$JOINED"
|
|
137
|
+
lines+=(" $(basename "$t" .csproj): $v")
|
|
138
|
+
case "$v" in PASS*) passed=$((passed+1)) ;; FAIL*) fails+=("$(basename "$t" .csproj)") ;; *) notrun+=("$(basename "$t" .csproj)") ;; esac
|
|
139
|
+
done
|
|
140
|
+
if [[ ${#fails[@]} -gt 0 ]]; then
|
|
141
|
+
echo "FAIL tests failed in ${fails[*]} (${#tests[@]} test projects, each run on its own; $passed passed); log $JOINED"; rc=1
|
|
142
|
+
elif [[ ${#notrun[@]} -gt 0 ]]; then
|
|
143
|
+
echo "NOT-RUN ${notrun[*]} could not run (${#tests[@]} test projects, each run on its own; $passed passed); log $JOINED"; rc=2
|
|
144
|
+
else
|
|
145
|
+
echo "PASS test on $PLATFORM — ${#tests[@]} test projects, each run on its own, all passed; log $JOINED"; rc=0
|
|
146
|
+
fi
|
|
147
|
+
printf '%s\n' "${lines[@]}"
|
|
148
|
+
exit $rc
|
|
149
|
+
fi
|
|
150
|
+
fi
|
|
151
|
+
|
|
111
152
|
# ---- the dotnet verb -------------------------------------------------------------------
|
|
112
153
|
verb="$MODE"
|
|
113
154
|
args=("$verb")
|
|
@@ -162,6 +203,26 @@ cross_side() { # side: clear what another side built with its own paths in it, t
|
|
|
162
203
|
}
|
|
163
204
|
WRONG_RUNG='NETSDK1178|Microsoft\.(iOS|Android|MacCatalyst|tvOS)\.Sdk|Workload ID|not recognized|WindowsAppSDK|command not found|No such file or directory|is not recognized as an internal or external command|The term .* is not recognized|workload.*not installed|Inadequate permissions'
|
|
164
205
|
|
|
206
|
+
# A Mac Catalyst build after Platforms/MacCatalyst/Info.plist (or an .entitlements file) changed: the
|
|
207
|
+
# incremental build refreshed the DLLs and kept the .app's old Info.plist, so a fix that lives there
|
|
208
|
+
# looked as if it had failed (Lekhak TF-026: the scene manifest was in the source and not in the
|
|
209
|
+
# bundle). When such a file is newer than the bundle, that target's bin/ and obj/ are cleared first.
|
|
210
|
+
mac_tfm="$(printf '%s\n' ${EXTRA[@]+"${EXTRA[@]}"} | grep -oE '^net[0-9.]+-maccatalyst[0-9.]*$' | head -1)"
|
|
211
|
+
if [[ -n "$mac_tfm" && "$MODE" == build && "$TARGET" == *.csproj ]]; then
|
|
212
|
+
pd="$(dirname "$TARGET")"
|
|
213
|
+
for cfgdir in "$pd"/bin/*/"$mac_tfm"; do
|
|
214
|
+
[[ -d "$cfgdir" ]] || continue
|
|
215
|
+
plist="$(ls -t "$cfgdir"/*.app/Contents/Info.plist "$cfgdir"/*/*.app/Contents/Info.plist 2>/dev/null | head -1)"
|
|
216
|
+
[[ -n "$plist" ]] || continue
|
|
217
|
+
newer="$(find "$pd/Platforms/MacCatalyst" -maxdepth 1 \( -name '*.plist' -o -name '*.entitlements' \) -newer "$plist" 2>/dev/null | head -1)"
|
|
218
|
+
if [[ -n "$newer" ]]; then
|
|
219
|
+
cfg="$(basename "$(dirname "$cfgdir")")"
|
|
220
|
+
rm -rf "$cfgdir" "$pd/obj/$cfg/$mac_tfm"
|
|
221
|
+
echo "note $newer is newer than the built app ($plist); cleared bin/$cfg/$mac_tfm and obj/$cfg/$mac_tfm so the bundle is built again"
|
|
222
|
+
fi
|
|
223
|
+
done
|
|
224
|
+
fi
|
|
225
|
+
|
|
165
226
|
tried=()
|
|
166
227
|
n=0
|
|
167
228
|
for r in "${rungs[@]}"; do
|
|
@@ -183,6 +244,20 @@ for r in "${rungs[@]}"; do
|
|
|
183
244
|
[[ "$rc" != "0" ]] && grep -qE "$LOCKED" <<<"$seg" || break
|
|
184
245
|
[[ $attempt -lt ${TF_BUILD_LOCK_RETRIES:-2} ]] && sleep "${TF_BUILD_LOCK_WAIT:-15}"
|
|
185
246
|
done
|
|
247
|
+
# A Mac Catalyst SDK names the one Xcode it was made for and refuses a newer one ("requires Xcode
|
|
248
|
+
# 26.6. The current version of Xcode is 27.0"): not the code, not the rung, so it fell through to
|
|
249
|
+
# NOT-RUN, while tf-verify-boot.sh built the same head with the check off (Lekhak TF-032). The same
|
|
250
|
+
# rung runs once more with the check off, and a note line says so before the verdict.
|
|
251
|
+
if [[ "$rc" != "0" && "$PLATFORM" == macos ]] && grep -qE 'requires Xcode [0-9.]+[.] The current version of Xcode is' <<<"$seg" \
|
|
252
|
+
&& [[ " ${cmd[*]} " != *ValidateXcodeVersion* ]]; then
|
|
253
|
+
xnote="$(grep -oE 'requires Xcode [0-9.]+[.] The current version of Xcode is [0-9.]+' <<<"$seg" | head -1 | sed 's/[.]$//')"
|
|
254
|
+
echo "note the Mac Catalyst SDK $xnote; built again with -p:ValidateXcodeVersion=false (xcode-check=off)"
|
|
255
|
+
cmd+=(-p:ValidateXcodeVersion=false)
|
|
256
|
+
from=$(( $(wc -l < "$LOG") + 1 ))
|
|
257
|
+
{ echo "### rung $n, Xcode check off: ${cmd[*]}"; "${cmd[@]}" 2>&1; echo "### exit $?"; } >> "$LOG" 2>&1
|
|
258
|
+
rc="$(tail -1 "$LOG" | sed 's/### exit //')"
|
|
259
|
+
seg="$(tail -n +"$from" "$LOG")"
|
|
260
|
+
fi
|
|
186
261
|
if [[ "$rc" == "0" ]]; then
|
|
187
262
|
warns="$(grep -cE 'warning [A-Z]+[0-9]+' <<<"$seg" || true)"
|
|
188
263
|
echo "PASS $MODE on $PLATFORM via $label (rung $n) — $warns warning line(s); log $LOG"
|
|
@@ -72,8 +72,26 @@ def set_cells(path, rid, status=None, pct=None, remark=None, append_remark=None)
|
|
|
72
72
|
return prior
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
def
|
|
76
|
-
|
|
75
|
+
def siblings(path):
|
|
76
|
+
"""every checklist of the same application: docs/<App>-Checklist.md and docs/<App>-P<N>-Checklist.md"""
|
|
77
|
+
d, f = os.path.split(path)
|
|
78
|
+
m = re.match(r"^(.+?)(?:-P\d+)?-Checklist\.md$", f)
|
|
79
|
+
if not m:
|
|
80
|
+
return []
|
|
81
|
+
app = re.escape(m.group(1))
|
|
82
|
+
try:
|
|
83
|
+
names = os.listdir(d or ".")
|
|
84
|
+
except OSError:
|
|
85
|
+
return []
|
|
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
|
+
|
|
88
|
+
|
|
89
|
+
def next_id(text, prefix, others=()):
|
|
90
|
+
"""one past the highest id of that prefix in this checklist AND every other checklist of the app:
|
|
91
|
+
counting this one alone gave REQ-UI-055 twice, once in phase 1 and once in phase 3 (Chatur TF-011)"""
|
|
92
|
+
ids = []
|
|
93
|
+
for t in (text, *(read(o) for o in others)):
|
|
94
|
+
ids += [int(m.group(1)) for m in re.finditer(rf"REQ-{prefix}-(\d{{3,}})", t)]
|
|
77
95
|
return f"REQ-{prefix}-{(max(ids) + 1 if ids else 1):03d}"
|
|
78
96
|
|
|
79
97
|
|
|
@@ -81,7 +99,7 @@ def add_row(path, prefix, title, acceptance, section=None, remark=None, status="
|
|
|
81
99
|
"""append a row to the Requirements Status table and a detail entry under `section`
|
|
82
100
|
(a new section at the end when it does not exist); returns the new id"""
|
|
83
101
|
text = read(path)
|
|
84
|
-
rid = next_id(text, prefix)
|
|
102
|
+
rid = next_id(text, prefix, siblings(path))
|
|
85
103
|
nl = "\r\n" if "\r\n" in text else "\n"
|
|
86
104
|
lines = text.split(nl)
|
|
87
105
|
# the table: after the last REQ row
|
|
@@ -4,15 +4,17 @@
|
|
|
4
4
|
bash .tfcore/utils/tf-day1-files.sh <App> [--size S|M|L] [--kind app|library] [--prefix obj|none] [--phase N]
|
|
5
5
|
bash .tfcore/utils/tf-day1-files.sh --archive <file> [<file> ...]
|
|
6
6
|
|
|
7
|
-
Default mode, for <App
|
|
8
|
-
- .tfcore/core-config.yaml
|
|
9
|
-
|
|
7
|
+
Default mode, for <App>. Each flag does its own part and nothing else:
|
|
8
|
+
- --size, --kind, --phase set appSize, appKind, appPhase in .tfcore/core-config.yaml; --phase is the
|
|
9
|
+
phase being worked (handoff moves it on).
|
|
10
10
|
- --size L also writes docs/<App>-Phases.md from app-phases-tmpl.md when it does not exist
|
|
11
11
|
(the Large layout: docs/TechieFlow-Document-Schemas.md §2, §3.11). The agent fills the table.
|
|
12
|
-
- --
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
- --prefix obj|none (day-1 stage 2) writes .editorconfig (overwritten; machine config), AGENTS.md and
|
|
13
|
+
CLAUDE.md from their templates with {AppName} substituted and the field-prefix line resolved, an
|
|
14
|
+
existing copy archived first; and adds the brd, architecture and codingStandards paths to
|
|
15
|
+
customTechnicalDocuments and devLoadAlwaysFiles where missing, keeping every entry already there.
|
|
16
|
+
Raising the size of a project with *amend-docs (--size L) once replaced Sevak's AGENTS.md and CLAUDE.md
|
|
17
|
+
with blank templates and cut its document list to three entries (Sevak TF-001). tests/regression sv_001.
|
|
16
18
|
--archive: move each file (and its sibling .html) to docs/OldDocs/, date-suffixed when a
|
|
17
19
|
file of that name is already there. Never asks; never writes a -v2 variant.
|
|
18
20
|
|
|
@@ -86,22 +88,45 @@ def set_key(text, key, value_lines):
|
|
|
86
88
|
return "\n".join(out) + "\n"
|
|
87
89
|
|
|
88
90
|
|
|
89
|
-
def
|
|
91
|
+
def block_of(text, key):
|
|
92
|
+
"""The indented lines under a top-level YAML key, [] when the key is absent or empty."""
|
|
93
|
+
lines, out, inside = text.splitlines(), [], False
|
|
94
|
+
for line in lines:
|
|
95
|
+
if re.match(rf"^{re.escape(key)}:", line):
|
|
96
|
+
inside = True
|
|
97
|
+
continue
|
|
98
|
+
if inside:
|
|
99
|
+
if line.startswith((" ", "\t")):
|
|
100
|
+
out.append(line)
|
|
101
|
+
elif line.strip():
|
|
102
|
+
break
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def add_documents(s, app):
|
|
107
|
+
"""The day-1 document paths, added where missing; every entry already listed is kept."""
|
|
108
|
+
docs = block_of(s, "customTechnicalDocuments")
|
|
109
|
+
have = {m.group(1) for m in (re.match(r"^\s+([\w-]+):", l) for l in docs) if m}
|
|
110
|
+
for k, v in (("brd", f"docs/{app}-BRD.md"), ("architecture", f"docs/{app}-Architecture.md"),
|
|
111
|
+
("codingStandards", f"docs/{app}-Coding-Standards.md")):
|
|
112
|
+
if k not in have:
|
|
113
|
+
docs.append(f" {k}: {v}")
|
|
114
|
+
s = set_key(s, "customTechnicalDocuments", ["customTechnicalDocuments:"] + docs)
|
|
115
|
+
files = block_of(s, "devLoadAlwaysFiles")
|
|
116
|
+
listed = {l.strip().lstrip("-").strip() for l in files}
|
|
117
|
+
for f in (f"docs/{app}-Coding-Standards.md", f"docs/{app}-Architecture.md"):
|
|
118
|
+
if f not in listed:
|
|
119
|
+
files.append(f" - {f}")
|
|
120
|
+
return set_key(s, "devLoadAlwaysFiles", ["devLoadAlwaysFiles:"] + files)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def update_config(app, size, kind, phase, documents):
|
|
90
124
|
p = os.path.join(".tfcore", "core-config.yaml")
|
|
91
125
|
if not os.path.isfile(p):
|
|
92
126
|
die(f"{p} does not exist at its literal path; is the framework installed here?")
|
|
93
127
|
s = read(p)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
f" brd: docs/{app}-BRD.md",
|
|
97
|
-
f" architecture: docs/{app}-Architecture.md",
|
|
98
|
-
f" codingStandards: docs/{app}-Coding-Standards.md",
|
|
99
|
-
])
|
|
100
|
-
s = set_key(s, "devLoadAlwaysFiles", [
|
|
101
|
-
"devLoadAlwaysFiles:",
|
|
102
|
-
f" - docs/{app}-Coding-Standards.md",
|
|
103
|
-
f" - docs/{app}-Architecture.md",
|
|
104
|
-
])
|
|
128
|
+
if documents:
|
|
129
|
+
s = add_documents(s, app)
|
|
105
130
|
if size:
|
|
106
131
|
s = set_key(s, "appSize", [f"appSize: {size}"])
|
|
107
132
|
if kind:
|
|
@@ -109,9 +134,9 @@ def update_config(app, size, kind, phase):
|
|
|
109
134
|
if phase:
|
|
110
135
|
s = set_key(s, "appPhase", [f"appPhase: {phase}"])
|
|
111
136
|
write(p, s)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
137
|
+
done = ([f"documents for {app}"] if documents else []) + ([f"appSize {size}"] if size else []) \
|
|
138
|
+
+ ([f"appKind {kind}"] if kind else []) + ([f"appPhase {phase}"] if phase else [])
|
|
139
|
+
print(f"tf-day1-files: .tfcore/core-config.yaml updated ({', '.join(done)})")
|
|
115
140
|
|
|
116
141
|
|
|
117
142
|
def write_phases_doc(app):
|
|
@@ -161,8 +186,7 @@ def main(argv):
|
|
|
161
186
|
app = argv[1]
|
|
162
187
|
if not re.fullmatch(r"[A-Z][A-Za-z0-9]*", app):
|
|
163
188
|
die(f"app name {app!r} must be PascalCase with no spaces")
|
|
164
|
-
size = kind = phase = None
|
|
165
|
-
prefix = "obj"
|
|
189
|
+
size = kind = phase = prefix = None
|
|
166
190
|
args = argv[2:]
|
|
167
191
|
i = 0
|
|
168
192
|
while i < len(args):
|
|
@@ -190,12 +214,15 @@ def main(argv):
|
|
|
190
214
|
i += 2
|
|
191
215
|
else:
|
|
192
216
|
die(f"unknown argument {a!r}")
|
|
193
|
-
|
|
217
|
+
if not (size or kind or phase or prefix):
|
|
218
|
+
die("nothing to do: give --size, --kind, --phase or --prefix")
|
|
219
|
+
update_config(app, size, kind, phase, documents=bool(prefix))
|
|
194
220
|
if size == "L":
|
|
195
221
|
write_phases_doc(app)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
222
|
+
if prefix: # day-1 stage 2 only: a size, kind or phase change never rewrites these (Sevak TF-001)
|
|
223
|
+
write_from_template(app, "app-editorconfig-tmpl.editorconfig", ".editorconfig", None)
|
|
224
|
+
write_from_template(app, "app-agents-md-tmpl.md", "AGENTS.md", prefix)
|
|
225
|
+
write_from_template(app, "app-claude-md-tmpl.md", "CLAUDE.md", prefix)
|
|
199
226
|
return 0
|
|
200
227
|
|
|
201
228
|
|