Add source linter, negative test fixtures, and fallback guidance

- scripts/lint.py: reject raw rectangles, absolute coordinates, hue-budget
  and callout/group/formula-order violations at the source level
- tests/invalid/ + tests/lint-invalid/: negative fixtures proving the
  package and linter reject bad input; test.sh now runs both directions
- references/fallback.md: degraded path when no LaTeX is available
- tests/group-callout.tex: exercise \stgroup and \stcallout
- agents/openai.yaml: agent config
- Docs and .sty updated to match
This commit is contained in:
dela
2026-08-05 16:41:22 +08:00
parent 7b59c81d02
commit 866173a831
34 changed files with 786 additions and 54 deletions
+3
View File
@@ -58,6 +58,9 @@
\stcaption{Wc}{$\mathbf W^{(r)}$}{$d\times T$}
\stcaption{Z}{$\mathbf Z$}{$T\times T$}
\sttopformula{F}{$\displaystyle
\operatorname{flow}(X,W)\;:\;\text{对象按自身边界框依次占位}$}
\stbbox{all}
\stmeaningbox{mb}{15cm}{all}
{$T$ 序列长,$d$ 模型维,$r$ 低秩维}
+78
View File
@@ -0,0 +1,78 @@
% Regression test for \stgroup and \stcallout.
% - a group over one stack (the three sheets are one composite object),
% - a group over two adjacent shards (the pair is one partitioned parent),
% - a side card hanging off the right edge of each band.
% No absolute coordinate appears anywhere in this figure.
% ../scripts/build.sh group-callout.tex
\documentclass[border=10pt]{standalone}
\usepackage[cjk]{supertensor}
\stsetrole{act}{stTeal}
\stsetrole{w}{stOrange}
\stsetrole{q}{stTeal}
\stsetrole{k}{stViolet}
\stsetrole{v}{stCoral}
\stdim{T}{7}
\stdim{d}{5}
\stdim{dh}{3}
\begin{document}
\begin{tikzpicture}
\ststage{S1}{一、分组框:把已经排好的面命名为一个复合对象}
\strow{R1}{T}
\stface[role=act, bracket=true]{X}{}{T}{d}
\stlink{l1}{三路投影}
\stgroup[role=q]{QG}
\ststack[role=q]{Q}{}{T}{dh}{3}
\stgroupend
\stgroup[role=k]{KG}
\ststack[role=k]{K}{}{T}{dh}{3}
\stgroupend
\stgroup[role=v]{VG}
\ststack[role=v]{V}{}{T}{dh}{3}
\stgroupend
\strowend
\stcaption{X}{$\mathbf X$}{$T\times d$}
\stcaption{QG}{$\mathbf q$}{$h\times T\times d_h$}
\stcaption{KG}{$\mathbf k$}{$h\times T\times d_h$}
\stcaption{VG}{$\mathbf v$}{$h\times T\times d_h$}
\stcallout{N1}{4.2cm}{R1}{分组框在说什么}%
{外框与里面的面同色,因为它圈的是同一个对象的多头视图,不是一个新张量。
框的内边距计入行的包围盒,所以下面的符号轨仍然从框底起算。}
\ststage{S2}{二、分组框圈住两片相邻的分片,标注它们合起来是谁}
\strow{R2}{T}
\stface[role=act]{H}{}{T}{d}
\stglyph{m2}{$\times$}
\stgroup[role=w, pad=2.2mm]{WG}
\stcol{Wc}{d}
\stface[role=w]{Wa}{}{dh}{T}
\stface[role=w, gap=0pt]{Wb}{}{2}{T}
\stcolend
\stgroupend
\stlink{l2}{}
\stface[role=q, bracket=true]{Z}{}{T}{T}
\strowend
\stcaption{H}{$\mathbf H$}{$T\times d$}
\stcaption{WG}{$\mathbf W$}{$d\times T$}
\stcaption{Z}{$\mathbf Z$}{$T\times T$}
\stcallout{N2}{4.2cm}{R2}{一行一张卡}%
{同一个 band 上挂两张卡会被 lint 拒绝:并排的说明卡是仪表盘,不是图。}
\sttopformula{F}{$\displaystyle \mathbf Z=\mathbf H\,\mathbf W,\qquad
\mathbf W=\begin{bmatrix}\mathbf W_a\\ \mathbf W_b\end{bmatrix}$}
\stbbox{all}
\stmeaningbox{mb}{17cm}{all}
{$T$ 序列长,$d$ 模型维,$d_h$ 头宽,$h$ 头数}
{}
{分组框是横向的子流:成员写在块里,所以它只能圈住相邻的对象;
两侧的内边距各占一份宽度,箭头也接在外框上而不是成员上——
否则连接线会穿过一个并非自己端点的框。
说明卡挂在 band 右侧,并把纵向游标压到自己底边以下,所以卡片过高只会撑开空白,
不会压住下一段。}
\end{tikzpicture}
\end{document}
+7
View File
@@ -0,0 +1,7 @@
\documentclass[border=2pt]{standalone}
\usepackage[en]{supertensor}
\stdim{d}{4}
\stdim{d}{7}
\begin{document}\begin{tikzpicture}
\stface{A}{(0,0)}{d}{d}
\end{tikzpicture}\end{document}
+8
View File
@@ -0,0 +1,8 @@
\documentclass[border=2pt]{standalone}
\usepackage[en]{supertensor}
\stsetrole{idx}{stOrange}
\stdim{m}{2}
\stdim{n}{3}
\begin{document}\begin{tikzpicture}
\stindexface[role=idx]{I}{(0,0)}{m}{n}{0,1,2,3,4}
\end{tikzpicture}\end{document}
+8
View File
@@ -0,0 +1,8 @@
\documentclass[border=2pt]{standalone}
\usepackage[en]{supertensor}
\stsetrole{x}{stTeal}
\stdim{m}{2}
\stdim{n}{3}
\begin{document}\begin{tikzpicture}
\stface[role=x,pattern=data,data={303}]{A}{(0,0)}{m}{n}
\end{tikzpicture}\end{document}
+8
View File
@@ -0,0 +1,8 @@
\documentclass[border=2pt]{standalone}
\usepackage[en]{supertensor}
\stsetrole{x}{stTeal}
\stdim{m}{2}
\stdim{n}{3}
\begin{document}\begin{tikzpicture}
\stface[role=x,pattern=data,data={303,34}]{A}{(0,0)}{m}{n}
\end{tikzpicture}\end{document}
+7
View File
@@ -0,0 +1,7 @@
\documentclass[border=2pt]{standalone}
\usepackage[en]{supertensor}
\stsetrole{x}{stTeal}
\stdim{d}{3}
\begin{document}\begin{tikzpicture}
\stface[role=x,pattern=solid,level=9]{A}{(0,0)}{d}{d}
\end{tikzpicture}\end{document}
+7
View File
@@ -0,0 +1,7 @@
\documentclass[border=2pt]{standalone}
\usepackage[en]{supertensor}
\stsetrole{x}{stTeal}
\stdim{d}{3}
\begin{document}\begin{tikzpicture}
\stface[role=x,pattern=checkerboard]{A}{(0,0)}{d}{d}
\end{tikzpicture}\end{document}
+8
View File
@@ -0,0 +1,8 @@
\documentclass[border=2pt]{standalone}
\usepackage[en]{supertensor}
\stsetrole{x}{stTeal}
\stsetrole{x}{stCoral}
\stdim{d}{3}
\begin{document}\begin{tikzpicture}
\stface[role=x]{A}{(0,0)}{d}{d}
\end{tikzpicture}\end{document}
+7
View File
@@ -0,0 +1,7 @@
\documentclass{standalone}
\usepackage[en]{supertensor}
\stdim{d}{3}
\begin{document}\begin{tikzpicture}
\stface{A}{(0,0)}{d}{d}
\sttopformula{F}{$A$}
\end{tikzpicture}\end{document}
+12
View File
@@ -0,0 +1,12 @@
% A callout anchored to a face rather than to a band: the card lands beside a
% single operand and reads as a step in the computation.
\documentclass{standalone}
\usepackage[en]{supertensor}
\stdim{T}{4}
\begin{document}\begin{tikzpicture}
\strow{R1}{T}
\stface{A}{}{T}{T}
\strowend
\stcallout{N1}{3cm}{A}{aside}{anchored to a face, not to the band}
\sttopformula{F}{$A$}
\end{tikzpicture}\end{document}
+13
View File
@@ -0,0 +1,13 @@
% Two asides on one band: a dashboard, not a figure. The second card belongs
% in \stmeaningbox.
\documentclass{standalone}
\usepackage[en]{supertensor}
\stdim{T}{4}
\begin{document}\begin{tikzpicture}
\strow{R1}{T}
\stface{A}{}{T}{T}
\strowend
\stcallout{N1}{3cm}{R1}{first}{one aside per band}
\stcallout{N2}{3cm}{R1}{second}{this one has nowhere to go}
\sttopformula{F}{$A$}
\end{tikzpicture}\end{document}
+9
View File
@@ -0,0 +1,9 @@
\documentclass{standalone}
\usepackage[en]{supertensor}
\stdim{d}{3}
\begin{document}\begin{tikzpicture}
\sttopformula{F}{$A$}
\strow{row}{d}
\stface{A}{}{d}{d}
\strowend
\end{tikzpicture}\end{document}
+12
View File
@@ -0,0 +1,12 @@
% An unclosed \stgroup swallows the rest of the band into its fit list; TeX
% only reports the resulting empty row, which does not point at the cause.
\documentclass{standalone}
\usepackage[en]{supertensor}
\stdim{T}{4}
\begin{document}\begin{tikzpicture}
\strow{R1}{T}
\stgroup{G}
\stface{A}{}{T}{T}
\strowend
\sttopformula{F}{$A$}
\end{tikzpicture}\end{document}
+18
View File
@@ -0,0 +1,18 @@
\documentclass{standalone}
\usepackage[en]{supertensor}
\stsetrole{a}{stTeal}
\stsetrole{b}{stOrange}
\stsetrole{c}{stCoral}
\stsetrole{d}{stViolet}
\stsetrole{e}{stInk}
\stdim{x}{2}
\begin{document}\begin{tikzpicture}
\strow{row}{x}
\stface[role=a]{A}{}{x}{x}
\stface[role=b]{B}{}{x}{x}
\stface[role=c]{C}{}{x}{x}
\stface[role=d]{D}{}{x}{x}
\stface[role=e]{E}{}{x}{x}
\strowend
\sttopformula{F}{$A+B+C+D+E$}
\end{tikzpicture}\end{document}
+6
View File
@@ -0,0 +1,6 @@
\documentclass{standalone}
\usepackage[en]{supertensor}
\begin{document}\begin{tikzpicture}
\fill (0,0) rectangle (2,2);
\sttopformula{F}{$A$}
\end{tikzpicture}\end{document}
+2
View File
@@ -1,3 +1,5 @@
% supertensor-lint: allow-absolute, allow-missing-formula
% Legacy absolute-placement smoke test; flow layout is covered by flow.tex.
\documentclass[border=8pt]{standalone}
\usepackage[cjk]{supertensor}