@techierathore/techieflow 1.2.0 → 1.2.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.
|
@@ -43,8 +43,8 @@ if not isinstance(cmd, str) or not cmd.strip():
|
|
|
43
43
|
WRITE_VERBS = r"\b(insert|update|delete|create|alter|drop|truncate|grant|revoke|replace|merge|upsert)\b"
|
|
44
44
|
CLIENTS = r"\b(psql|pgcli|sqlite3|mysql|mariadb|sqlcmd|mongosh|mongo)\b"
|
|
45
45
|
MIGRATORS = (r"\bdotnet\s+ef\s+database\s+(update|drop)\b"
|
|
46
|
-
r"|\bdotnet\s+run\b[^|;&]*--project\s+\S*(db|migration|migrations)\b"
|
|
47
|
-
r"|\
|
|
46
|
+
r"|\bdotnet\s+run\b[^|;&]*--project\s+\S*(db|dbup|migration|migrations)\b"
|
|
47
|
+
r"|\bflyway\s+(migrate|clean|undo)\b|\bliquibase\s+(update|rollback|drop-all)\b"
|
|
48
48
|
r"|\balembic\s+(upgrade|downgrade)\b|\bprisma\s+(migrate|db\s+push)\b|\bknex\s+migrate\b"
|
|
49
49
|
r"|\brails\s+db:(migrate|reset|drop|schema:load)\b|\bsequelize\s+db:migrate\b|\bgoose\s+(up|down|reset)\b")
|
|
50
50
|
DROP_DB = r"\b(dropdb|drop\s+database|mongo(sh)?\b[^|;&]*dropDatabase)\b"
|
|
@@ -75,6 +75,10 @@ def phase():
|
|
|
75
75
|
SEG = (r"(?:^|[;&|(`\n]|\$\()\s*(?:sudo\s+(?:-\S+\s+)*)?(?:env\s+(?:\S+=\S*\s+)*)?"
|
|
76
76
|
r"(?:[\w./\\-]*\bexec\s+(?:-\S+\s+)*\S+\s+)?")
|
|
77
77
|
CLIENT_AT = SEG + r"(?:\S*/)?" + CLIENTS
|
|
78
|
+
# DbUp is a library: it runs as the command word of a segment (`dbup upgrade`, `./App.DbUp.exe`,
|
|
79
|
+
# `dotnet App.DbUp.dll`) or through `dotnet run --project …DbUp`. A bare word "dbup" anywhere blocked a
|
|
80
|
+
# miss sentence naming DbUp and every grep for it (Chatur TF-014).
|
|
81
|
+
MIGRATORS += r"|" + SEG + r"(?:dotnet\s+)?(?:\S*[/\\.])?dbup(?:-cli)?\b"
|
|
78
82
|
INTERP = r"(?:python3?|py|node|deno|bun|ruby|perl|pwsh|powershell(?:\.exe)?|tsx|ts-node)"
|
|
79
83
|
INTERP_AT = SEG + r"(?:\S*/)?" + INTERP + r"\b"
|
|
80
84
|
DB_LIBS = (r"\bsqlite3\b|better-sqlite3|\bsql\.js\b|node:sqlite|\bpsycopg|\bpymysql\b|mysql\.connector|\bmysql2\b"
|
|
@@ -171,7 +175,14 @@ if direct_write or re.search(DROP_DB, low):
|
|
|
171
175
|
"is fine. Owner rule 2026-09-05.", file=sys.stderr)
|
|
172
176
|
sys.exit(2)
|
|
173
177
|
|
|
174
|
-
|
|
178
|
+
# a quoted argument is a mention (a miss sentence, a search pattern), unless it is the code a shell runs
|
|
179
|
+
# (`bash -c "…"`) or the project `dotnet run --project` builds (Chatur TF-014)
|
|
180
|
+
def _mention(m):
|
|
181
|
+
keep = re.search(r"(?:\s-c|\s/c|\s-command|--project|-p)\s*=?\s*$", low[:m.start()])
|
|
182
|
+
return m.group(0) if keep else '""'
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if re.search(MIGRATORS, re.sub(r"\"(?:[^\"\\]|\\.)*\"|'[^']*'", _mention, low)):
|
|
175
186
|
ph = phase()
|
|
176
187
|
# triage-and-fix step 3 is fix-issues steps 2 to 5 under its own marker (AppManager TF-027)
|
|
177
188
|
if ph in ("build-phase", "fix-issues", "triage-and-fix"):
|
|
@@ -201,6 +201,31 @@ cross_side() { # side: clear what another side built with its own paths in it, t
|
|
|
201
201
|
done
|
|
202
202
|
[[ $cleared -eq 1 ]] && echo "note the scoped stylesheets and static asset lists were built on the other side of this machine; cleared so the $1 build writes them itself"
|
|
203
203
|
}
|
|
204
|
+
# Lekhak TF-036: on WSL a target with no MAUI head starts on the WSL rungs. A test project inside a solution
|
|
205
|
+
# that has a MAUI head is built Windows-side with that solution, and ~/.dotnet/dotnet then could not write
|
|
206
|
+
# the obj/ Windows owns (MSB3491): tf-doc-tests.sh said NOT-RUN where `tf-build.sh test` on the solution
|
|
207
|
+
# had just passed. Such a target starts on the side its solution builds on, the Windows side. Outside such
|
|
208
|
+
# a solution, a target whose obj/ (or a referenced project's) was last built Windows-side starts there.
|
|
209
|
+
# The record alone is not enough: a failed WSL attempt writes it before it fails.
|
|
210
|
+
if [[ "$PLATFORM" == "wsl" && $maui -eq 0 && "$TARGET" == *.csproj ]]; then
|
|
211
|
+
won=""; tname="$(basename "$TARGET")"
|
|
212
|
+
for sln in *.slnx *.sln; do
|
|
213
|
+
[[ -f "$sln" ]] && grep -q "$tname" "$sln" || continue
|
|
214
|
+
while IFS= read -r p; do
|
|
215
|
+
[[ -f "$p" ]] && grep -qiE '<UseMaui>[[:space:]]*true|Microsoft\.NET\.Sdk\.Maui|net[0-9.]+-(android|ios|maccatalyst|windows10)' "$p" \
|
|
216
|
+
&& { won="$sln has a MAUI head, so it builds on the Windows side"; break 2; }
|
|
217
|
+
done < <(grep -oE '"[^"]+\.csproj"|Path="[^"]+\.csproj"' "$sln" | sed 's/^Path=//' | tr -d '"' | tr '\\' '/')
|
|
218
|
+
done
|
|
219
|
+
if [[ -z "$won" ]]; then
|
|
220
|
+
for d in ${pdirs[@]+"${pdirs[@]}"}; do
|
|
221
|
+
[[ "$(cat "$d/obj/.tf-build-side" 2>/dev/null)" == windows ]] && { won="$d/obj was last built on the Windows side"; break; }
|
|
222
|
+
done
|
|
223
|
+
fi
|
|
224
|
+
if [[ -n "$won" ]]; then
|
|
225
|
+
rungs=("cmd.exe" "winrun" "powershell.exe" "dotnet" "$HOME/.dotnet/dotnet")
|
|
226
|
+
echo "note $won; this $MODE starts there too (rungs: cmd.exe, winrun, powershell.exe, then the WSL ones)"
|
|
227
|
+
fi
|
|
228
|
+
fi
|
|
204
229
|
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'
|
|
205
230
|
|
|
206
231
|
# A Mac Catalyst build after Platforms/MacCatalyst/Info.plist (or an .entitlements file) changed: the
|
|
@@ -246,7 +246,9 @@ const PROBE = (wanted = []) => {
|
|
|
246
246
|
if (!c) return null;
|
|
247
247
|
const { r, g, b } = c;
|
|
248
248
|
const mx = Math.max(r, g, b), mn = Math.min(r, g, b);
|
|
249
|
-
|
|
249
|
+
// a grey with a tint is still a grey: Sevak's dark nav text #a6adc5 (spread 31, saturation 21%) read as
|
|
250
|
+
// accent beside the app's near-white, once TF-007 had the mockup read settled and not mid-fade
|
|
251
|
+
if (mx - mn < 28 || (mx - mn) / (255 - Math.abs(mx + mn - 255)) < 0.25) return 'neutral';
|
|
250
252
|
// By hue, never by one channel. "Red is highest, so green above 120 is a warning" put a deep
|
|
251
253
|
// amber (180, 83, 9) in `negative` beside a light amber in `warning` — one warning tone on two
|
|
252
254
|
// sides of the line, reported as a colour difference on TfLens /misses (TF-045).
|
|
@@ -662,7 +664,13 @@ const CLAUSES = {
|
|
|
662
664
|
: m.token.fits && !a.token.fits
|
|
663
665
|
? `value cell is narrower than its longest unbreakable token ("${a.token.token}") — it breaks mid-token`
|
|
664
666
|
: false),
|
|
667
|
+
// Sevak TF-010: a sample box the app draws is graded on being there with a size, nothing it says
|
|
668
|
+
size: (m, a) => (!m.in_sample ? null
|
|
669
|
+
: m.w > 0 && m.h > 0 && (!(a.w > 0) || !(a.h > 0)) ? `the mockup draws this sample box ${m.w}×${m.h}; the app draws it ${a.w ?? 0}×${a.h ?? 0}` : false),
|
|
665
670
|
};
|
|
671
|
+
// Sevak TF-010: inside a box the mockup marks data-tf-sample, what the sample says is the app's real data
|
|
672
|
+
// ("Yes, it can run" against this PC's truthful "No"), so its colour, its text and its badge are not graded
|
|
673
|
+
const SAMPLE_SKIP = new Set(['color', 'badge', 'wrap', 'token']);
|
|
666
674
|
|
|
667
675
|
// Which clauses require having reached INSIDE a container rather than merely
|
|
668
676
|
// touching its outer box. TF-011's `harness` screen graded three column CONTAINERS
|
|
@@ -800,8 +808,10 @@ function diff(mock, app, screen, width, skip = null) {
|
|
|
800
808
|
return false;
|
|
801
809
|
};
|
|
802
810
|
|
|
811
|
+
const sampleKeys = Object.keys(mock.index).filter((k) => mock.index[k].sample);
|
|
803
812
|
for (const key of Object.keys(mock.index)) {
|
|
804
|
-
const
|
|
813
|
+
const inSample = sampleKeys.some((k) => key === k || key.startsWith(k + ' > '));
|
|
814
|
+
const m = inSample ? { ...mock.index[key], in_sample: true } : mock.index[key];
|
|
805
815
|
let a = app.index[key];
|
|
806
816
|
let appKey = key;
|
|
807
817
|
|
|
@@ -829,6 +839,7 @@ function diff(mock, app, screen, width, skip = null) {
|
|
|
829
839
|
// wrapper div and become the always-present finding TF-012 warns about — the
|
|
830
840
|
// fastest way to train a reader to skim the whole report.
|
|
831
841
|
if (!a) {
|
|
842
|
+
if (inSample && m.badge === true) continue; // a sample badge reading otherwise in the app is its data (TF-010)
|
|
832
843
|
const parentKey = key.includes(' > ') ? key.slice(0, key.lastIndexOf(' > ')) : null;
|
|
833
844
|
const parentPaired = parentKey ? !!app.index[parentKey] : false;
|
|
834
845
|
// an icon is missing only when the paired parent carries fewer icons than the mockup's
|
|
@@ -870,6 +881,7 @@ function diff(mock, app, screen, width, skip = null) {
|
|
|
870
881
|
compared++;
|
|
871
882
|
for (const [name, fn] of Object.entries(CLAUSES)) {
|
|
872
883
|
if (skip && skip.has(name)) continue; // a clause the mockup says nothing about in this theme (TF-024)
|
|
884
|
+
if (inSample && SAMPLE_SKIP.has(name)) continue;
|
|
873
885
|
const r = fn(m, a);
|
|
874
886
|
if (r === null) continue;
|
|
875
887
|
clauseCoverage[name]++;
|
|
@@ -72,11 +72,23 @@ const RESTORE = (before) => {
|
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
+
const SETTLE_ANIMATIONS = () => {
|
|
76
|
+
let n = 0;
|
|
77
|
+
for (const a of document.getAnimations()) { // getAnimations() flushes style, so a fade the change just started is in it
|
|
78
|
+
try { if (Number.isFinite(a.effect?.getComputedTiming().endTime)) { a.finish(); n++; } } catch (e) { /* not finishable */ }
|
|
79
|
+
}
|
|
80
|
+
return n;
|
|
81
|
+
};
|
|
82
|
+
|
|
75
83
|
// Draws the page in `want` and returns the function that puts it back.
|
|
76
84
|
export async function useTheme(page, want, settle = 350) {
|
|
77
85
|
await page.emulateMedia({ colorScheme: want }).catch(() => {});
|
|
78
86
|
const before = await page.evaluate(FORCE, want).catch(() => null);
|
|
79
87
|
if (settle) await page.waitForTimeout(settle);
|
|
88
|
+
// Sevak TF-007: a theme change starts the page's CSS fades (`transition: all .12s`), and the mockup,
|
|
89
|
+
// read with no settle, gave its light colour in dark mode. Every fade and finite animation is run to
|
|
90
|
+
// its end, so what is read is the colour the page settles on; a spinner (infinite) is left running.
|
|
91
|
+
await page.evaluate(SETTLE_ANIMATIONS).catch(() => {});
|
|
80
92
|
return async () => {
|
|
81
93
|
if (before) await page.evaluate(RESTORE, before).catch(() => {});
|
|
82
94
|
await page.emulateMedia({ colorScheme: null }).catch(() => {});
|
|
@@ -24,7 +24,8 @@ For each row, the seven checks in order, and the first that fails is the verdict
|
|
|
24
24
|
Verdicts: PASS · FAIL (acceptance) · BUILD-FAIL · RENDER-FAIL · ASSET-FAIL · VISUAL-FAIL · MOCKUP-FAIL ·
|
|
25
25
|
PERF-FAIL · NOT-TESTED (no test carried the id, or every test carrying it was skipped; the screen
|
|
26
26
|
checks passed or did not run) ·
|
|
27
|
-
NOT-OBSERVABLE (an NFR row with no budget and no unit test) · NOT-
|
|
27
|
+
NOT-OBSERVABLE (an NFR row with no budget and no unit test) · NOT-BUILT (a Not Started row whose only
|
|
28
|
+
failing check is its route answering 404: it keeps Not Started and emits no miss, Chatur TF-013) · NOT-DRIVEN (its screen was never driven:
|
|
28
29
|
the head could not be booted, or has no driver) · DEFECT-OPEN (every check passed, but the old Remarks
|
|
29
30
|
carry a defect no verify can see, tf_defect.py: the row goes to Needs re-verify, and so to a fix, with
|
|
30
31
|
the defect kept; AppManager TF-021). A row is Verified only on PASS. NOT-* rows keep their
|
|
@@ -174,8 +175,16 @@ def main(argv):
|
|
|
174
175
|
# it. It is neither a pass nor a defect, so it grades exactly like a row with no test at
|
|
175
176
|
# all — never Verified, no gate record (TF-022). `t` is kept for the note.
|
|
176
177
|
skipped_only = bool(t) and t.get("result") == "NOT-TESTED"
|
|
178
|
+
# Sevak TF-009: a row whose unit tests passed while its on-app test was skipped (Sevak TF-004)
|
|
179
|
+
# was written "no unit test carrying its id"; the tests ran, they just do not count
|
|
180
|
+
unit_only = ""
|
|
181
|
+
if skipped_only and t.get("passed"):
|
|
182
|
+
why = re.sub(r"^on-app test skipped [(](.*)[)]; a unit test.*$", r"\1", t.get("reason") or "")
|
|
183
|
+
unit_only = f"{t['passed']} unit test(s) named {rid} passed but do not count while its on-app test was skipped" \
|
|
184
|
+
+ (f": {why}" if why else "")
|
|
177
185
|
if skipped_only:
|
|
178
|
-
|
|
186
|
+
if not unit_only:
|
|
187
|
+
notes.append(f"acceptance not measured ({len(t.get('skipped') or [])} test(s) skipped, none ran)")
|
|
179
188
|
t = None
|
|
180
189
|
# the OS zoom keys (coding-standards-dotnet.md §9b): a row whose acceptance carries "zoom-check: keys"
|
|
181
190
|
# or "zoom-check: kept" is graded by tf-verify-zoom.sh --json-out zoom/<REQ>.json, which stands as its
|
|
@@ -201,6 +210,7 @@ def main(argv):
|
|
|
201
210
|
if t:
|
|
202
211
|
checks.append(("acceptance", True, t["result"] != "PASS", "FAIL", "assert-fail", quoted(t.get("reason")) or "test failed", t.get("screenshot") or ""))
|
|
203
212
|
# 3 render
|
|
213
|
+
bad = [] # Chatur TF-015: an UNREACHABLE first screen left it unset for the Mac note below
|
|
204
214
|
if scr:
|
|
205
215
|
if scr.get("render") == "UNREACHABLE":
|
|
206
216
|
f = next((x for w in scr["widths"] for x in w.get("findings", [])), {})
|
|
@@ -280,12 +290,25 @@ def main(argv):
|
|
|
280
290
|
notes.append(f"perf not measured: {g['reason']}")
|
|
281
291
|
|
|
282
292
|
ran = [c[0] for c in checks if c[1]]
|
|
283
|
-
|
|
293
|
+
failed = [c for c in checks if c[1] and c[2]]
|
|
294
|
+
first = failed[0] if failed else None
|
|
284
295
|
verdict, cls, detail, evidence = (first[3], first[4], first[5], first[6]) if first else (None, None, "", "")
|
|
296
|
+
# Chatur TF-013: a Not Started row whose only failing check is its route answering 404 (the mockup
|
|
297
|
+
# comparison could not open it either, for the same reason) was never built; it is not a defect to
|
|
298
|
+
# re-verify, so it keeps Not Started and emits no miss
|
|
299
|
+
if verdict == "RENDER-FAIL" and all(re.search(r"HTTP 404\b", c[5] or "") for c in failed) \
|
|
300
|
+
and scr and scr.get("render") == "UNREACHABLE" \
|
|
301
|
+
and (r.get("status") or prior).strip("`* ").lower().startswith("not started") \
|
|
302
|
+
and all(re.search(r"answered HTTP 404\b", f.get("detail", "")) for w in scr["widths"] for f in w.get("findings", [])[:1]):
|
|
303
|
+
verdict, cls, evidence = "NOT-BUILT", None, ""
|
|
304
|
+
detail = f"{detail}: the screen is not built yet, nothing to re-verify"
|
|
285
305
|
|
|
286
306
|
# the honest non-verdicts
|
|
287
307
|
if verdict is None:
|
|
288
|
-
if
|
|
308
|
+
if unit_only:
|
|
309
|
+
verdict = "NOT-TESTED"
|
|
310
|
+
detail = unit_only + (f"; screen {r['screen']} renders and looks right{nonames}" if driven else "")
|
|
311
|
+
elif r["class"] == "NFR" and not r.get("perf_budget") and not t:
|
|
289
312
|
verdict = "NOT-OBSERVABLE"
|
|
290
313
|
detail = "NFR row with no perf-budget and no unit test carrying its id"
|
|
291
314
|
elif not t and r.get("screen") and not driven:
|
|
@@ -338,6 +361,7 @@ def main(argv):
|
|
|
338
361
|
marks = {"FAIL": "⚠ acceptance", "BUILD-FAIL": "⚠ build", "RENDER-FAIL": "⚠ render", "ASSET-FAIL": "⚠ assets",
|
|
339
362
|
"VISUAL-FAIL": "⚠ visual", "MOCKUP-FAIL": "⚠ mockup-parity", "PERF-FAIL": "⚠ perf",
|
|
340
363
|
"NOT-TESTED": "not verified", "NOT-OBSERVABLE": "not observable", "NOT-DRIVEN": "not verified", "PASS": "PASS",
|
|
364
|
+
"NOT-BUILT": "not built",
|
|
341
365
|
"DEFECT-OPEN": "checks pass, defect open"}
|
|
342
366
|
remark = f"{today} verify: {marks[verdict]} — {detail}"
|
|
343
367
|
if evidence:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@techierathore/techieflow",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "TechieFlow: a spec-driven development framework for AI coding agents, for Claude Code and OpenCode. One command installs it into any project.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|