Compare commits
6 Commits
4272fd2deb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f2dc7acf5 | ||
|
|
6aa226c53e | ||
|
|
5aee19d16d | ||
|
|
4c2bbd5393 | ||
|
|
13640338d2 | ||
|
|
7345ba49ba |
374
DESIGN.md
374
DESIGN.md
@@ -363,7 +363,7 @@ _Pipeline stages resolved so far; printer/transport (F4) and F3's preview mechan
|
|||||||
- **F1** Minimise capture effort _↓ ≤1 gesture, ≤10 s_
|
- **F1** Minimise capture effort _↓ ≤1 gesture, ≤10 s_
|
||||||
- **How (chosen):** one **CLI engine** invoked by a **per-editor keystroke** that pipes the selection and passes `--file`/`--lines` (Vim map, JetBrains External Tool, VS Code/Cursor keybinding→task). **Xcode:** clipboard-only fallback, no line numbers. _Chosen over a per-editor extension — see T1._
|
- **How (chosen):** one **CLI engine** invoked by a **per-editor keystroke** that pipes the selection and passes `--file`/`--lines` (Vim map, JetBrains External Tool, VS Code/Cursor keybinding→task). **Xcode:** clipboard-only fallback, no line numbers. _Chosen over a per-editor extension — see T1._
|
||||||
- **F2** Fidelity & legibility _↑_
|
- **F2** Fidelity & legibility _↑_
|
||||||
- **How (chosen):** CLI renders selection → image via its own tokenizer (tree-sitter / `silicon`-class), sized to the 90×85 mm cell. Monospace makes fit a deterministic **character budget** (~48 cols × ~19 rows at 8 pt after margins + footer); the CLI flags selections that exceed it. Font picked for legibility at the printer's DPI (label ≈ 300 dpi).
|
- **How (chosen):** CLI renders selection → image via its own tokenizer (tree-sitter / `silicon`-class), sized to the 90×85 mm cell. Monospace makes fit a deterministic **character budget** (~48 cols × ~19 rows at 8 pt after margins + footer); the CLI flags selections that exceed it. Font picked for legibility at the printer's DPI (label ≈ 300 dpi). The defect itself is **marked** on the card — one or more character-precise spans (box / bold / highlight / underline), sourced from the editor selection(s); see [SPEC.md](./SPEC.md).
|
||||||
- **F3** Scope to the cell _→_
|
- **F3** Scope to the cell _→_
|
||||||
- **How (chosen):** F3a — CLI renders the real card **at exact cell size (90×85 mm), cell boundary drawn**, and previews it: **inline** in graphics-capable terminals (iTerm2 / Kitty / **Ghostty** / WezTerm / VS Code-Cursor sixel), or as a **text-grid** (code clamped to the ~48-col budget — instant, no graphics needed), or the PNG opened in the OS viewer. Overflow spills past the border → F2's "flag, don't shrink" made visual. Confirm-to-print; `-y` skips. WYSIWYG because the preview *is* the print artifact.
|
- **How (chosen):** F3a — CLI renders the real card **at exact cell size (90×85 mm), cell boundary drawn**, and previews it: **inline** in graphics-capable terminals (iTerm2 / Kitty / **Ghostty** / WezTerm / VS Code-Cursor sixel), or as a **text-grid** (code clamped to the ~48-col budget — instant, no graphics needed), or the PNG opened in the OS viewer. Overflow spills past the border → F2's "flag, don't shrink" made visual. Confirm-to-print; `-y` skips. WYSIWYG because the preview *is* the print artifact.
|
||||||
- **G3** Traceable to source _W:6_
|
- **G3** Traceable to source _W:6_
|
||||||
@@ -413,7 +413,7 @@ The second house: Functions (HOWs) become the WHATs, realised by concrete compon
|
|||||||
| C1 | `dp` CLI engine — **Rust, `clap`** (capture stdin+`--file`/`--lines`, render, preview, emit) | ADR-0001, ADR-0003 |
|
| C1 | `dp` CLI engine — **Rust, `clap`** (capture stdin+`--file`/`--lines`, render, preview, emit) | ADR-0001, ADR-0003 |
|
||||||
| C2 | Per-editor keystroke shims (Vim / JetBrains / VS Code·Cursor; Xcode clipboard) | ADR-0001 |
|
| C2 | Per-editor keystroke shims (Vim / JetBrains / VS Code·Cursor; Xcode clipboard) | ADR-0001 |
|
||||||
| C3 | Tokenizing renderer — **`syntect`/`silicon`-class → mono PNG at cell size**, `viuer` inline preview | ADR-0003 |
|
| C3 | Tokenizing renderer — **`syntect`/`silicon`-class → mono PNG at cell size**, `viuer` inline preview | ADR-0003 |
|
||||||
| C4 | Central print service — **Python / `brother_ql`** (board→printer registry, queue, status, router) | ADR-0002 |
|
| C4 | Central print service — **Python / `brother_ql`** (board→printer registry, queue, status, router) | ADR-0002, ADR-0004 |
|
||||||
| C5 | Networked label printer — Brother QL-1110NWB, per board | ADR-0002 |
|
| C5 | Networked label printer — Brother QL-1110NWB, per board | ADR-0002 |
|
||||||
| C6 | Board registry / config (`--board`, per-repo/dev default) | ADR-0002 |
|
| C6 | Board registry / config (`--board`, per-repo/dev default) | ADR-0002 |
|
||||||
|
|
||||||
@@ -429,6 +429,375 @@ Function → Component strength (9/3/1/blank):
|
|||||||
|
|
||||||
_F5's deferred SSO short-link redirector would later attach to **C4**._
|
_F5's deferred SSO short-link redirector would later attach to **C4**._
|
||||||
|
|
||||||
|
### House 2 — Functions × Components
|
||||||
|
|
||||||
|
Functions (House 1's HOWs) are now the WHATs, down the left with their carried-down
|
||||||
|
weights; components are the HOWs across the top. Roof = component↔component
|
||||||
|
correlation; basement = per-component target, build difficulty, and weight.
|
||||||
|
|
||||||
|
<!-- Self-contained: this ```tikz fence compiles independently, so it repeats
|
||||||
|
House 1's template preamble verbatim; only the overrides + body below differ. -->
|
||||||
|
|
||||||
|
```tikz
|
||||||
|
\usetikzlibrary{arrows.meta, positioning, shapes.geometric, shapes.misc, calc, fit, backgrounds}
|
||||||
|
|
||||||
|
% Toggles
|
||||||
|
\newif\ifqfdshowroof \qfdshowrooftrue
|
||||||
|
\newif\ifqfdshowbasement \qfdshowbasementtrue
|
||||||
|
\newif\ifqfdshowcompetitive \qfdshowcompetitivetrue
|
||||||
|
\newif\ifqfdshowlegend \qfdshowlegendtrue
|
||||||
|
\newif\ifqfdshowimportance \qfdshowimportancetrue
|
||||||
|
\newif\ifqfdshowcorrlegend \qfdshowcorrlegendtrue
|
||||||
|
\newif\ifqfdshowevallegend \qfdshowevallegendtrue
|
||||||
|
\newif\ifqfdshowtitle \qfdshowtitletrue
|
||||||
|
|
||||||
|
% Dimensions
|
||||||
|
\def\qfdNW{5}
|
||||||
|
\def\qfdNH{5}
|
||||||
|
\def\qfdWhatW{4.0}
|
||||||
|
\def\qfdImpW{0.9}
|
||||||
|
\def\qfdCmpW{3}
|
||||||
|
\def\qfdHdrH{2.6}
|
||||||
|
\def\qfdBasementN{4}
|
||||||
|
|
||||||
|
% Titles & labels
|
||||||
|
\def\qfdWhatsTitle{Customer needs}
|
||||||
|
\def\qfdImpTitle{Imp.\ \%}
|
||||||
|
\def\qfdPerceptionTitle{Comparative evaluation}
|
||||||
|
\def\qfdPoorLabel{poor}
|
||||||
|
\def\qfdExcellentLabel{excellent}
|
||||||
|
\def\qfdAltOneLabel{Our product}
|
||||||
|
\def\qfdAltTwoLabel{Competitor A}
|
||||||
|
\def\qfdAltThreeLabel{Competitor B}
|
||||||
|
\def\qfdRelTitle{Relation}
|
||||||
|
\def\qfdCorrTitle{Correlation}
|
||||||
|
\def\qfdEvalTitle{Evaluation}
|
||||||
|
|
||||||
|
\def\qfdProjectTitle{}
|
||||||
|
\def\qfdConcept{}
|
||||||
|
|
||||||
|
% Styles
|
||||||
|
\tikzset{
|
||||||
|
qfdthin/.style ={line width=0.35pt},
|
||||||
|
qfdmed/.style ={line width=0.7pt},
|
||||||
|
qfdstrong/.style={circle, draw, fill=black,
|
||||||
|
minimum size=7pt, inner sep=0pt},
|
||||||
|
qfdmod/.style ={circle, draw,
|
||||||
|
minimum size=7pt, inner sep=0pt, line width=0.8pt},
|
||||||
|
qfdweak/.style ={regular polygon, regular polygon sides=3, draw,
|
||||||
|
minimum size=8.5pt, inner sep=0pt, line width=0.7pt},
|
||||||
|
qfdrel/.is choice,
|
||||||
|
qfdrel/S/.style={qfdstrong},
|
||||||
|
qfdrel/M/.style={qfdmod},
|
||||||
|
qfdrel/W/.style={qfdweak},
|
||||||
|
qfdalt1mk/.style={circle, draw, fill=black,
|
||||||
|
minimum size=6pt, inner sep=0pt, line width=1pt},
|
||||||
|
qfdalt1ln/.style={line width=1.2pt},
|
||||||
|
qfdalt2mk/.style={regular polygon, regular polygon sides=3, draw,
|
||||||
|
fill=black, minimum size=6pt, inner sep=0pt,
|
||||||
|
line width=0.7pt},
|
||||||
|
qfdalt2ln/.style={line width=0.7pt, dashed},
|
||||||
|
qfdalt3mk/.style={rectangle, draw, fill=black,
|
||||||
|
minimum size=5pt, inner sep=0pt, line width=0.7pt},
|
||||||
|
qfdalt3ln/.style={line width=0.7pt, dotted},
|
||||||
|
}
|
||||||
|
|
||||||
|
\newcommand{\qfdDrawGrid}{%
|
||||||
|
\foreach \c in {1,...,\qfdNHm} \draw[qfdthin] (\c, 0) -- (\c, -\qfdNW);
|
||||||
|
\foreach \r in {1,...,\qfdNWm} \draw[qfdthin] (0, -\r) -- (\qfdNH, -\r);
|
||||||
|
\foreach \r in {1,...,\qfdNWm}
|
||||||
|
\draw[qfdthin] (\qfdLeftEdge, -\r) -- (0, -\r);
|
||||||
|
\ifqfdshowroof
|
||||||
|
\foreach \c in {1,...,\qfdNHm}
|
||||||
|
\draw[qfdthin] (\c, 0) -- (\c, \qfdHdrH);
|
||||||
|
\fi
|
||||||
|
\ifqfdshowcompetitive
|
||||||
|
\foreach \r in {1,...,\qfdNWm}
|
||||||
|
\draw[qfdthin] (\qfdNH, -\r) -- (\qfdNH+\qfdCmpW, -\r);
|
||||||
|
\fi
|
||||||
|
\ifqfdshowbasement
|
||||||
|
\foreach \r in {1,...,\qfdBasementN}
|
||||||
|
\draw[qfdthin] (0, -\qfdNW-\r) -- (\qfdNH, -\qfdNW-\r);
|
||||||
|
\foreach \c in {1,...,\qfdNHm}
|
||||||
|
\draw[qfdthin] (\c, -\qfdNW) -- (\c, -\qfdNW-\qfdBasementN);
|
||||||
|
\fi
|
||||||
|
}
|
||||||
|
|
||||||
|
\newcommand{\qfdDrawRoof}{%
|
||||||
|
\ifqfdshowroof
|
||||||
|
\foreach \k in {1,...,\qfdNHm} {%
|
||||||
|
\pgfmathsetmacro{\rx}{(\k+\qfdNH)/2}
|
||||||
|
\pgfmathsetmacro{\ry}{\qfdHdrH + (\qfdNH-\k)/2}
|
||||||
|
\pgfmathsetmacro{\lx}{\k/2}
|
||||||
|
\pgfmathsetmacro{\ly}{\qfdHdrH + \k/2}
|
||||||
|
\draw[qfdthin] (\k, \qfdHdrH) -- (\rx, \ry);
|
||||||
|
\draw[qfdthin] (\k, \qfdHdrH) -- (\lx, \ly);
|
||||||
|
}%
|
||||||
|
\draw[qfdmed] (0, \qfdHdrH)
|
||||||
|
-- (\qfdNH/2, \qfdApexY) -- (\qfdNH, \qfdHdrH);
|
||||||
|
\foreach \i in {1,...,\qfdNH}
|
||||||
|
\foreach \k in {1,...,\qfdNH} {%
|
||||||
|
\pgfmathtruncatemacro{\jj}{\i+\k}
|
||||||
|
\ifnum\jj>\qfdNH\relax\else
|
||||||
|
\pgfmathsetmacro{\xx}{\i + \k/2 - 0.5}
|
||||||
|
\pgfmathsetmacro{\yy}{\qfdHdrH + \k/2}
|
||||||
|
\coordinate (C-\i-\jj) at (\xx, \yy);
|
||||||
|
\fi
|
||||||
|
}%
|
||||||
|
\fi
|
||||||
|
}
|
||||||
|
|
||||||
|
\newcommand{\qfdDrawScale}{%
|
||||||
|
\ifqfdshowcompetitive
|
||||||
|
\foreach \tk in {0,1,2,3,4,5} {%
|
||||||
|
\pgfmathsetmacro{\tx}{\qfdNH + (\tk+0.5)*\qfdCmpW/6}
|
||||||
|
\node[anchor=south, font=\scriptsize] at (\tx, 0.02) {\tk};
|
||||||
|
}%
|
||||||
|
\node[anchor=south, font=\scriptsize\bfseries, align=center]
|
||||||
|
at ({\qfdNH + \qfdCmpW/2}, 0.7) {\qfdPerceptionTitle};
|
||||||
|
\node[anchor=north, font=\scriptsize\itshape]
|
||||||
|
at ({\qfdNH + 0.45}, -\qfdNW) {\qfdPoorLabel};
|
||||||
|
\node[anchor=north, font=\scriptsize\itshape]
|
||||||
|
at ({\qfdNH + \qfdCmpW - 0.45}, -\qfdNW) {\qfdExcellentLabel};
|
||||||
|
\fi
|
||||||
|
}
|
||||||
|
|
||||||
|
\newcommand{\qfdDrawZoneTitles}{%
|
||||||
|
\ifqfdshowimportance
|
||||||
|
\node[rotate=90, anchor=west, font=\footnotesize\bfseries]
|
||||||
|
at ({-\qfdImpW/2}, 0.12) {\qfdImpTitle};
|
||||||
|
\fi
|
||||||
|
\node[font=\scriptsize\bfseries, align=center, text width=\qfdWhatW cm]
|
||||||
|
at ({\qfdLeftEdge + \qfdWhatW/2},
|
||||||
|
{\ifqfdshowroof \qfdHdrH/2 \else 0.6 \fi}) {\qfdWhatsTitle};
|
||||||
|
}
|
||||||
|
|
||||||
|
\newcommand{\qfdDrawTitle}{%
|
||||||
|
\ifqfdshowtitle
|
||||||
|
\ifx\qfdProjectTitle\empty\else
|
||||||
|
\pgfmathsetmacro{\qfdTitleX}{\qfdNH/2}
|
||||||
|
\pgfmathsetmacro{\qfdTitleY}{\ifqfdshowroof \qfdApexY \else \qfdHdrH \fi + 0.9}
|
||||||
|
\pgfmathsetmacro{\qfdSubW}{\qfdNH + 2}
|
||||||
|
\node[anchor=south, font=\large\bfseries, align=center]
|
||||||
|
at (\qfdTitleX, \qfdTitleY) {\qfdProjectTitle};
|
||||||
|
\ifx\qfdConcept\empty\else
|
||||||
|
\node[anchor=north, font=\footnotesize\itshape, align=center,
|
||||||
|
text width=\qfdSubW cm]
|
||||||
|
at (\qfdTitleX, {\qfdTitleY - 0.1}) {\qfdConcept};
|
||||||
|
\fi
|
||||||
|
\fi
|
||||||
|
\fi
|
||||||
|
}
|
||||||
|
|
||||||
|
\newcommand{\qfdDrawFrames}{%
|
||||||
|
\begin{scope}[qfdmed]
|
||||||
|
\draw (\qfdLeftEdge, 0) rectangle (\qfdNH, -\qfdNW);
|
||||||
|
\ifqfdshowimportance \draw (-\qfdImpW, 0) -- (-\qfdImpW, -\qfdNW); \fi
|
||||||
|
\draw (0, 0) -- (0, -\qfdNW);
|
||||||
|
\ifqfdshowroof
|
||||||
|
\draw (0, 0) rectangle (\qfdNH, \qfdHdrH); \fi
|
||||||
|
\ifqfdshowbasement
|
||||||
|
\draw (0, -\qfdNW) rectangle (\qfdNH, -\qfdNW-\qfdBasementN); \fi
|
||||||
|
\ifqfdshowcompetitive
|
||||||
|
\draw (\qfdNH, 0) rectangle (\qfdNH+\qfdCmpW, -\qfdNW); \fi
|
||||||
|
\end{scope}
|
||||||
|
}
|
||||||
|
|
||||||
|
\newcommand{\qfdDrawLegend}{%
|
||||||
|
\ifqfdshowlegend
|
||||||
|
\pgfmathsetmacro{\qfdLegX}{%
|
||||||
|
\qfdNH + \ifqfdshowcompetitive \qfdCmpW + 0.7 \else 0.7 \fi}
|
||||||
|
\pgfmathsetmacro{\qfdLegBottom}{%
|
||||||
|
-2.05
|
||||||
|
\ifqfdshowroof \ifqfdshowcorrlegend - 2.55 \fi \fi
|
||||||
|
\ifqfdshowcompetitive \ifqfdshowevallegend - 2.20 \fi \fi}
|
||||||
|
\pgfmathsetmacro{\qfdLegY}{\qfdHdrH - 0.4}
|
||||||
|
\begin{scope}[shift={(\qfdLegX, \qfdLegY)}]
|
||||||
|
\draw[qfdmed, rounded corners=2pt]
|
||||||
|
(-0.15, 0.4) rectangle (4.5, \qfdLegBottom);
|
||||||
|
\node[anchor=west, font=\footnotesize\bfseries] at (0, 0.1)
|
||||||
|
{\qfdRelTitle};
|
||||||
|
\draw[qfdthin] (0, -0.15) -- (4.35, -0.15);
|
||||||
|
\node[qfdstrong] at (0.22, -0.5) {};
|
||||||
|
\node[anchor=west] at (0.5, -0.5) {Strong (9)};
|
||||||
|
\node[qfdmod] at (0.22, -0.95) {};
|
||||||
|
\node[anchor=west] at (0.5, -0.95) {Medium (3)};
|
||||||
|
\node[qfdweak] at (0.22, -1.4) {};
|
||||||
|
\node[anchor=west] at (0.5, -1.4) {Weak (1)};
|
||||||
|
\ifqfdshowroof \ifqfdshowcorrlegend
|
||||||
|
\node[anchor=west, font=\footnotesize\bfseries] at (0, -2.10)
|
||||||
|
{\qfdCorrTitle};
|
||||||
|
\draw[qfdthin] (0, -2.35) -- (4.35, -2.35);
|
||||||
|
\node[anchor=west] at (0, -2.70) {{$+\!+$}\quad very positive};
|
||||||
|
\node[anchor=west] at (0, -3.05) {{$+$\phantom{$+$}}\quad positive};
|
||||||
|
\node[anchor=west] at (0, -3.40) {{$-$\phantom{$-$}}\quad negative};
|
||||||
|
\node[anchor=west] at (0, -3.75) {{$-\!-$}\quad very negative};
|
||||||
|
\fi \fi
|
||||||
|
\ifqfdshowcompetitive \ifqfdshowevallegend
|
||||||
|
\pgfmathsetmacro{\qfdEvalTop}{%
|
||||||
|
-2.10 \ifqfdshowroof\ifqfdshowcorrlegend - 2.55 \fi\fi}
|
||||||
|
\node[anchor=west, font=\footnotesize\bfseries]
|
||||||
|
at (0, \qfdEvalTop) {\qfdEvalTitle};
|
||||||
|
\pgfmathsetmacro{\qfdEvalSep}{\qfdEvalTop - 0.25}
|
||||||
|
\draw[qfdthin] (0, \qfdEvalSep) -- (4.35, \qfdEvalSep);
|
||||||
|
\pgfmathsetmacro{\qfdLegA}{\qfdEvalTop - 0.55}
|
||||||
|
\draw[qfdalt1ln] (0.05, \qfdLegA) -- (0.45, \qfdLegA);
|
||||||
|
\node[qfdalt1mk] at (0.25, \qfdLegA) {};
|
||||||
|
\node[anchor=west, font=\bfseries] at (0.55, \qfdLegA)
|
||||||
|
{\qfdAltOneLabel};
|
||||||
|
\pgfmathsetmacro{\qfdLegB}{\qfdEvalTop - 0.95}
|
||||||
|
\draw[qfdalt2ln] (0.05, \qfdLegB) -- (0.45, \qfdLegB);
|
||||||
|
\node[qfdalt2mk] at (0.25, \qfdLegB) {};
|
||||||
|
\node[anchor=west] at (0.55, \qfdLegB) {\qfdAltTwoLabel};
|
||||||
|
\pgfmathsetmacro{\qfdLegC}{\qfdEvalTop - 1.35}
|
||||||
|
\draw[qfdalt3ln] (0.05, \qfdLegC) -- (0.45, \qfdLegC);
|
||||||
|
\node[qfdalt3mk] at (0.25, \qfdLegC) {};
|
||||||
|
\node[anchor=west] at (0.55, \qfdLegC) {\qfdAltThreeLabel};
|
||||||
|
\fi \fi
|
||||||
|
\end{scope}
|
||||||
|
\fi
|
||||||
|
}
|
||||||
|
|
||||||
|
\newenvironment{qfdhouse}{%
|
||||||
|
\begin{tikzpicture}[x=1cm, y=1cm, font=\scriptsize,
|
||||||
|
line cap=round, line join=round]
|
||||||
|
\ifqfdshowimportance
|
||||||
|
\pgfmathsetmacro{\qfdLeftEdge}{-\qfdWhatW-\qfdImpW}
|
||||||
|
\else
|
||||||
|
\pgfmathsetmacro{\qfdLeftEdge}{-\qfdWhatW}
|
||||||
|
\fi
|
||||||
|
\pgfmathsetmacro{\qfdApexY}{\qfdHdrH + \qfdNH/2}
|
||||||
|
\pgfmathtruncatemacro{\qfdNHm}{\qfdNH - 1}
|
||||||
|
\pgfmathtruncatemacro{\qfdNWm}{\qfdNW - 1}
|
||||||
|
\qfdDrawGrid
|
||||||
|
\qfdDrawRoof
|
||||||
|
\qfdDrawScale
|
||||||
|
\qfdDrawZoneTitles
|
||||||
|
\qfdDrawTitle
|
||||||
|
}{%
|
||||||
|
\qfdDrawFrames
|
||||||
|
\qfdDrawLegend
|
||||||
|
\end{tikzpicture}%
|
||||||
|
}
|
||||||
|
|
||||||
|
% ---- Overrides for House 2 (Functions × Components) ----
|
||||||
|
\def\qfdNW{5}
|
||||||
|
\def\qfdNH{6}
|
||||||
|
\def\qfdWhatW{4.7}
|
||||||
|
\def\qfdHdrH{3.2}
|
||||||
|
\def\qfdImpTitle{Weight}
|
||||||
|
\def\qfdWhatsTitle{Functions (WHATs)}
|
||||||
|
\def\qfdProjectTitle{Dantotsu Print — House 2}
|
||||||
|
\def\qfdConcept{Functions (the HOWs of House 1) realised by concrete \textbf{components}.}
|
||||||
|
\qfdshowcompetitivefalse
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
\begin{qfdhouse}
|
||||||
|
% WHATs (Functions) + carried-down weights (House 1 relative weights)
|
||||||
|
\pgfmathsetmacro{\qfdWhatTextW}{\qfdWhatW - 0.2}
|
||||||
|
\foreach \r/\t in {1/{F1 Minimise capture effort},
|
||||||
|
2/{F2 Fidelity \& legibility},
|
||||||
|
3/{F3 Scope to the cell},
|
||||||
|
4/{F4 Minimise operating burden},
|
||||||
|
5/{F5 Durable back-link}}
|
||||||
|
\node[anchor=west, font=\scriptsize, text width=\qfdWhatTextW cm, align=left]
|
||||||
|
at ({\qfdLeftEdge + 0.1}, {-\r + 0.5}) {\t};
|
||||||
|
\foreach \r/\imp in {1/26, 2/26, 3/9, 4/24, 5/16}
|
||||||
|
\node[font=\scriptsize] at ({-\qfdImpW/2}, {-\r + 0.5}) {\imp};
|
||||||
|
|
||||||
|
% HOWs (Components)
|
||||||
|
\foreach \c/\t in {1/{C1 CLI engine}, 2/{C2 Keystroke shims},
|
||||||
|
3/{C3 Renderer}, 4/{C4 Print service},
|
||||||
|
5/{C5 Label printer}, 6/{C6 Board registry}}
|
||||||
|
\node[rotate=90, anchor=west, font=\scriptsize] at ({\c - 0.5}, 0.15) {\t};
|
||||||
|
|
||||||
|
% Relations (Function x Component, from the section 6 matrix)
|
||||||
|
\node[qfdrel/S] at ({1 - 0.5}, {-1 + 0.5}) {}; % F1-C1
|
||||||
|
\node[qfdrel/S] at ({2 - 0.5}, {-1 + 0.5}) {}; % F1-C2
|
||||||
|
\node[qfdrel/M] at ({6 - 0.5}, {-1 + 0.5}) {}; % F1-C6
|
||||||
|
\node[qfdrel/M] at ({1 - 0.5}, {-2 + 0.5}) {}; % F2-C1
|
||||||
|
\node[qfdrel/S] at ({3 - 0.5}, {-2 + 0.5}) {}; % F2-C3
|
||||||
|
\node[qfdrel/S] at ({1 - 0.5}, {-3 + 0.5}) {}; % F3-C1
|
||||||
|
\node[qfdrel/M] at ({3 - 0.5}, {-3 + 0.5}) {}; % F3-C3
|
||||||
|
\node[qfdrel/S] at ({4 - 0.5}, {-4 + 0.5}) {}; % F4-C4
|
||||||
|
\node[qfdrel/S] at ({5 - 0.5}, {-4 + 0.5}) {}; % F4-C5
|
||||||
|
\node[qfdrel/M] at ({6 - 0.5}, {-4 + 0.5}) {}; % F4-C6
|
||||||
|
\node[qfdrel/S] at ({1 - 0.5}, {-5 + 0.5}) {}; % F5-C1
|
||||||
|
\node[qfdrel/W] at ({4 - 0.5}, {-5 + 0.5}) {}; % F5-C4
|
||||||
|
|
||||||
|
% Roof correlations (Component x Component)
|
||||||
|
\node[font=\scriptsize] at (C-1-2) {$+\!+$}; % C1-C2 shims wrap the CLI
|
||||||
|
\node[font=\scriptsize] at (C-1-3) {$+\!+$}; % C1-C3 renderer in the CLI
|
||||||
|
\node[font=\scriptsize] at (C-1-4) {$+$}; % C1-C4 CLI POSTs to service
|
||||||
|
\node[font=\scriptsize] at (C-1-6) {$+$}; % C1-C6 CLI reads --board
|
||||||
|
\node[font=\scriptsize] at (C-3-4) {$+$}; % C3-C4 service transports raster
|
||||||
|
\node[font=\scriptsize] at (C-3-5) {$-$}; % C3-C5 mono printer caps renderer
|
||||||
|
\node[font=\scriptsize] at (C-4-5) {$+\!+$}; % C4-C5 service drives printer
|
||||||
|
\node[font=\scriptsize] at (C-4-6) {$+\!+$}; % C4-C6 registry lives in service
|
||||||
|
\node[font=\scriptsize] at (C-5-6) {$+$}; % C5-C6 registry maps board->printer
|
||||||
|
|
||||||
|
% Basement row labels
|
||||||
|
\node[anchor=west, font=\scriptsize] at ({\qfdLeftEdge + 0.1}, {-\qfdNW - 0.5}) {Target};
|
||||||
|
\node[anchor=west, font=\scriptsize] at ({\qfdLeftEdge + 0.1}, {-\qfdNW - 1.5}) {Difficulty (1--5)};
|
||||||
|
\node[anchor=west, font=\scriptsize] at ({\qfdLeftEdge + 0.1}, {-\qfdNW - 2.5}) {Abs.\ weight};
|
||||||
|
\node[anchor=west, font=\scriptsize] at ({\qfdLeftEdge + 0.1}, {-\qfdNW - 3.5}) {Rel.\ weight \%};
|
||||||
|
|
||||||
|
% Basement values
|
||||||
|
\foreach \c/\tgt/\diff/\abs/\rel in
|
||||||
|
{1/{$\leq$1 s}/4/537/33,
|
||||||
|
2/{4 eds}/2/234/14,
|
||||||
|
3/{300 dpi}/4/261/16,
|
||||||
|
4/{route}/3/232/14,
|
||||||
|
5/{102 mm}/1/216/13,
|
||||||
|
6/{board}/2/150/9} {
|
||||||
|
\node[font=\scriptsize] at ({\c - 0.5}, {-\qfdNW - 0.5}) {\tgt};
|
||||||
|
\node[font=\scriptsize] at ({\c - 0.5}, {-\qfdNW - 1.5}) {\diff};
|
||||||
|
\node[font=\scriptsize] at ({\c - 0.5}, {-\qfdNW - 2.5}) {\abs};
|
||||||
|
\node[font=\scriptsize\bfseries] at ({\c - 0.5}, {-\qfdNW - 3.5}) {\rel};
|
||||||
|
}
|
||||||
|
\end{qfdhouse}
|
||||||
|
\end{document}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Reading it.** **C1 (33%)** dominates — it realises four of five functions; it is
|
||||||
|
the hub, and with **C3 (16%)** carries the build risk (both Difficulty 4). C2/C4/C5
|
||||||
|
cluster ~13–14%, C6 trails at 9%. Effort should track weight.
|
||||||
|
|
||||||
|
#### House 2 roof — Component × Component
|
||||||
|
|
||||||
|
`◎` strong reinforce · `○` mild reinforce · `×` mild conflict.
|
||||||
|
|
||||||
|
| | C1 | C2 | C3 | C4 | C5 | C6 |
|
||||||
|
|--------|:--:|:--:|:--:|:--:|:--:|:--:|
|
||||||
|
| **C1** | — | ◎ | ◎ | ○ | | ○ |
|
||||||
|
| **C2** | | — | | | | |
|
||||||
|
| **C3** | | | — | ○ | × | |
|
||||||
|
| **C4** | | | | — | ◎ | ◎ |
|
||||||
|
| **C5** | | | | | — | ○ |
|
||||||
|
| **C6** | | | | | | — |
|
||||||
|
|
||||||
|
- **C1×C2 (◎), C1×C3 (◎)** — the shims are thin wrappers that invoke the CLI, and the renderer is a library inside it; building the CLI *is* building the seat these sit in.
|
||||||
|
- **C4×C5 (◎), C4×C6 (◎)** — the print service drives the printer via `brother_ql` and owns the board→printer registry; one coherent server.
|
||||||
|
- **C3×C5 (×) — the one conflict.** A richer renderer is capped by the monochrome ≥300 dpi / 102 mm label: hue and fine detail C3 *could* produce don't survive the print. This is **T2** (§8) made structural — spend renderer effort on legible monochrome, not colour.
|
||||||
|
- **Mild reinforces (○):** C1×C4 (CLI POSTs the card), C1×C6 (CLI reads `--board` to route), C3×C4 (service transports C3's raster — they must agree on its spec), C5×C6 (registry maps board→printer IP).
|
||||||
|
|
||||||
|
_Watched, not a roof conflict: **C1 (Rust) ↔ C4 (Python)** cooperate to print, but the two runtimes couple across an HTTP contract that must not drift — see **T6** (§8)._
|
||||||
|
|
||||||
|
#### House 2 basement — component performance budget
|
||||||
|
|
||||||
|
_Abs./Rel. weight are **computed** — §6 matrix × carried-down function weights
|
||||||
|
(F1 26, F2 26, F3 9, F4 24, F5 16). **Target** and **Difficulty** are a first
|
||||||
|
assessment introduced with this house; revise as ADRs/spikes land._
|
||||||
|
|
||||||
|
| Component | Target (now) | Difficulty | Abs. | Rel. % |
|
||||||
|
|-----------|--------------|:----------:|:----:|:------:|
|
||||||
|
| C1 CLI engine | single static binary; cold run ≤ ~1 s to emit | 4 | 537 | **33** |
|
||||||
|
| C3 Renderer | verbatim mono PNG at 90×85 mm, ~48c×19r @ 8 pt, crisp @300 dpi, span marks | 4 | 261 | **16** |
|
||||||
|
| C2 Keystroke shims| one map per editor (Vim/JetBrains/VS Code·Cursor); Xcode clipboard | 2 | 234 | **14** |
|
||||||
|
| C4 Print service | board→printer route + retries + status; service up | 3 | 232 | **14** |
|
||||||
|
| C5 Label printer | Brother QL-1110NWB, 102 mm, ≥300 dpi, removable, networked | 1 | 216 | **13** |
|
||||||
|
| C6 Board registry | `--board` + per-repo/dev default; board→printer map | 2 | 150 | **9** |
|
||||||
|
|
||||||
## 7. Critical performance budget
|
## 7. Critical performance budget
|
||||||
|
|
||||||
Ranked from §4 importance and §5 conflicts. `If we miss it` is mandatory — a target without a fallback is a wish.
|
Ranked from §4 importance and §5 conflicts. `If we miss it` is mandatory — a target without a fallback is a wish.
|
||||||
@@ -450,6 +819,7 @@ Ranked from §4 importance and §5 conflicts. `If we miss it` is mandatory — a
|
|||||||
| T3 | **Peel-stick label** (wide networked QL-1110NWB, removable), over magnet and over receipt+glue | bonded → no fall/misfile (G1 trust); zero manual step (F1); no ink (F4); CLI-driven via `brother_ql` | ~€9k/50 + recurring label rolls; wide model needed for width | — |
|
| T3 | **Peel-stick label** (wide networked QL-1110NWB, removable), over magnet and over receipt+glue | bonded → no fall/misfile (G1 trust); zero manual step (F1); no ink (F4); CLI-driven via `brother_ql` | ~€9k/50 + recurring label rolls; wide model needed for width | — |
|
||||||
| T4 | **T-B central service + networked printers**, over direct (T-A) / corporate CUPS (T-C) | central board→printer registry, retries, print/label status, `--board` routing, home for the F5 redirector; no per-board Pi to patch | a small service to run + maintain; single point of failure if down | ADR-0002 |
|
| T4 | **T-B central service + networked printers**, over direct (T-A) / corporate CUPS (T-C) | central board→printer registry, retries, print/label status, `--board` routing, home for the F5 redirector; no per-board Pi to patch | a small service to run + maintain; single point of failure if down | ADR-0002 |
|
||||||
| T5 | **Rust plain CLI**, over TS/Node and Gleam, no TUI framework | single-binary distribution to the polyglot fleet (F4); best-fit code→PNG ecosystem; no TUI machinery on the fire-and-exit path | Rust-comfortable maintainers required; interactive trim deferred to a scoped ratatui mode | ADR-0003 |
|
| T5 | **Rust plain CLI**, over TS/Node and Gleam, no TUI framework | single-binary distribution to the polyglot fleet (F4); best-fit code→PNG ecosystem; no TUI machinery on the fire-and-exit path | Rust-comfortable maintainers required; interactive trim deferred to a scoped ratatui mode | ADR-0003 |
|
||||||
|
| T6 | **C4 in Python (FastAPI + `brother_ql`)**, over Node/Hono, Deno/Oak, Gleam | native, battle-tested QL raster driver; one runtime; zero glue | Python in a TS-heavy shop; a second language beside the Rust CLI (clean HTTP boundary) | ADR-0004 |
|
||||||
|
|
||||||
### Tensions being watched (unresolved by design)
|
### Tensions being watched (unresolved by design)
|
||||||
|
|
||||||
|
|||||||
67
PLAN.md
Normal file
67
PLAN.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Plan: Dantotsu Print — `dp` CLI (v1)
|
||||||
|
|
||||||
|
> Phase 2 (Plan) artifact. Implements [SPEC.md](./SPEC.md). Reviewable: read it
|
||||||
|
> and say "right approach" or "change X" before we break it into tasks (Phase 3).
|
||||||
|
|
||||||
|
## Two tracks
|
||||||
|
|
||||||
|
- **Track A — `dp` CLI** (Rust, the critical path below).
|
||||||
|
- **Track B — C4 print service** (Python/FastAPI + `brother_ql`; built here,
|
||||||
|
owned by the project lead — ADR-0004). Runs **fully in parallel**: Track A is
|
||||||
|
built against a **mock `POST /print` stub** that honours the SPEC service
|
||||||
|
interface, so neither track blocks the other. The mock stub is swapped for the
|
||||||
|
real service at the **M6 ↔ CB1** integration point — the single sync point.
|
||||||
|
|
||||||
|
## Guiding principle
|
||||||
|
|
||||||
|
**Render first, print early.** The render is both the hard part *and* the only
|
||||||
|
physical unknown (small-size 300 dpi legibility, font). So the first slice
|
||||||
|
produces a real PNG you print and eyeball on an actual board — settling Open Q#1
|
||||||
|
before anything else is built on top of it.
|
||||||
|
|
||||||
|
## Milestones (Track A) — dependency-ordered
|
||||||
|
|
||||||
|
| # | Milestone | Depends on | Verification checkpoint |
|
||||||
|
|---|-----------|:----------:|-------------------------|
|
||||||
|
| **M0** | **Skeleton** — cargo crate, `clap` surface stubs (`dp`/`config`/`doctor`), `DpError` (`thiserror`), CI (`test`/`clippy -D warnings`/`fmt`) | — | `cargo build`; `dp --help` lists the three commands |
|
||||||
|
| **M1** | **Render slice (core)** — read `--file`/`--lines`, `layout` (cell budget + fit math), syntect → mono PNG at 300 dpi / 90×85 mm (JetBrains Mono, no ligatures), `--out` to disk | M0 | **Print `card.png` on the QL-1110NWB, read it at the board → settles Q#1 (font/legibility).** Golden test: fixture → committed golden PNG |
|
||||||
|
| **M2** | **Marking + overflow** — `Span::parse` (`L:C-L:C`, repeatable, multi-line), `emphasis` (box/bold/highlight/underline), context window (union of marks ± `--context`), overflow **abort** + `--force` truncation mark + drawn cell boundary | M1 | Goldens: sub-line box, multi-span, overflow-abort (exit code + "N over" message) |
|
||||||
|
| **M3** | **Back-link** — `gitctx`: `blame -L` → introducing SHA (fallback HEAD), repo slug from remote, footer band `owner/repo · path · La–b · @sha`, optional `--qr` | M1 | On a real repo, footer resolves to the introducing commit; `--qr` opens the permalink; unit tests parse blame output |
|
||||||
|
| **M4** | **Preview** — `viuer` inline (Kitty/iTerm/Ghostty/sixel) → text-grid → OS viewer; `--preview`; confirm `[y/N]`; `-y` skips | M1 | Inline in Ghostty; text-grid fallback; `-y` non-interactive; piped (no TTY) does not hang |
|
||||||
|
| **M5** | **Config + auth** — `config` TOML load/merge, board precedence (`--board` › repo `.dantotsu.toml` › user), `dp config set/show`; Google OAuth loopback (`dp login`/`logout`), token cache (`keyring` + config fallback), Bearer attach | M0 | Precedence unit tests; `dp login` completes OAuth against a test client; token cached + refreshed |
|
||||||
|
| **M6** | **Emit** — `emit` multipart `POST /print` (PNG + board + meta JSON), non-zero exit on failure, `--dry-run` skips | M2, M3, M5, mock stub | `--dry-run`; against the mock stub returns printer/job; failure path exits non-zero with an actionable message |
|
||||||
|
| **M7** | **Doctor + shims + packaging** — `doctor` (config/service/board→printer/terminal caps/git/font); shim recipes (`vim`, `jetbrains`, `vscode`, `xcode`); cross-compile binaries (macOS/Linux/Windows), bundle font | M4, M6 | `doctor` flags a broken config; each shim fires `dp` with correct args; binaries run on all three OSes |
|
||||||
|
|
||||||
|
## Milestones (Track B — C4 print service)
|
||||||
|
|
||||||
|
| # | Milestone | Depends on | Verification checkpoint |
|
||||||
|
|---|-----------|:----------:|-------------------------|
|
||||||
|
| **CB0** | **Skeleton + registry** — FastAPI app, board→printer registry config (`board-id → {printer_ip, model, media}`), `GET /boards/{id}` returning `{printer, status}` | — | `GET /boards/floor3-payments` returns the mapped printer |
|
||||||
|
| **CB1** | **Print path** — `POST /print` (multipart PNG + board + meta) → look up printer → PNG → QL raster (102 mm continuous, auto-cut) → `brother_ql` over the network | CB0 | **End-to-end print of a posted PNG on a real QL-1110NWB** *(physical gate — needs the printer)* |
|
||||||
|
| **CB2** | **Auth + status + deploy** — Google OIDC validation (audience + `hd=theodo.com`), label-out / reachability status, containerize & deploy to an internal host | CB1 | Bad/absent token rejected; unregistered board rejected; container runs on the internal host; `dp doctor` reads real status |
|
||||||
|
|
||||||
|
## Parallelism
|
||||||
|
|
||||||
|
Critical path: **M0 → M1 → M2 → M6 → M7.** Once **M1** lands, **M3, M4, M5** are
|
||||||
|
independent and can proceed concurrently (different devs / different sessions).
|
||||||
|
**Track B (CB0 → CB1 → CB2)** runs alongside the whole thing; Track A uses the
|
||||||
|
mock stub until the **M6 ↔ CB1** integration. Shim recipes (M7) can be drafted
|
||||||
|
anytime; they're only *tested* once M1–M6 work.
|
||||||
|
|
||||||
|
## Risks & mitigations
|
||||||
|
|
||||||
|
| ID | Risk | Mitigation |
|
||||||
|
|----|------|------------|
|
||||||
|
| R1 | **300 dpi small-size legibility** (font/size) — the physical unknown | M1's early print is a gate; if 8 pt JetBrains Mono isn't legible, swap font / adjust size+budget *before* goldens bake it in |
|
||||||
|
| R2 | **Rust code→PNG fidelity** (mono metrics, no shaping) | reuse silicon's approach; pin font + syntect grammar/theme versions for deterministic goldens; ligatures-off sidesteps shaping |
|
||||||
|
| R3 | **Google OAuth loopback across 3 OSes** (browser launch, localhost redirect, keychain — esp. Windows) | maintained `oauth2` crate; device-code fallback if loopback is awkward; `keyring` with config-file fallback |
|
||||||
|
| R4 | **Terminal graphics detection** varies (weakest on older Windows consoles) | robust fallback chain inline → text-grid → OS viewer; never hang when stdout isn't a TTY |
|
||||||
|
| R5 | **CLI↔C4 contract drift** | same owner; keep the mock stub authoritative to the SPEC interface and in the test suite |
|
||||||
|
| R6 | **Overflow abort annoyance** if snippets often overflow | clear "N over, narrow --lines" message; monitor at pilot (the F1×F3 tension is already logged in DESIGN §8) |
|
||||||
|
| R7 | **Printer IP churn** — DHCP reassigns the QL's address, rotting the registry (home test *and* prod) | give each printer a **static IP / DHCP reservation**; registry keys on it. In prod the service host must also route to each board's printer subnet |
|
||||||
|
|
||||||
|
## Definition of done (v1)
|
||||||
|
|
||||||
|
All SPEC success criteria pass; `dp` builds/runs on macOS+Linux+Windows; a real
|
||||||
|
selection in Vim/JetBrains prints a legible, correctly-marked, back-linked card
|
||||||
|
at the right board via C4; `dp doctor` is a working first line of support.
|
||||||
64
README.md
Normal file
64
README.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Dantotsu Print
|
||||||
|
|
||||||
|
Tooling that moves code-quality defects from the IDE onto the physical
|
||||||
|
"Daily — Dantotsu and Problem Solving" board with minimal friction, preserving
|
||||||
|
the Dantotsu ritual of *visualizing the defect* — which for developers means
|
||||||
|
seeing the actual lines of code.
|
||||||
|
|
||||||
|
A developer turns the code they're looking at into a **Defect card** — a
|
||||||
|
monochrome, peel-and-stick label sized to the ~90×85 mm **Defect** cell of a
|
||||||
|
physical A3 board — in one keystroke, without leaving the editor. The card
|
||||||
|
shows the verbatim code with the defect visually marked, plus a back-link to
|
||||||
|
the exact source (`path·lines·@sha` + QR). The machine prints only the Defect
|
||||||
|
cell; the human handwrites the causes, countermeasures, owner, and status.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**Design / spec stage — no code yet.** The vocabulary, design, spec, and key
|
||||||
|
decisions are settled (see [Documentation](#documentation)); implementation has
|
||||||
|
not started. The build plan is in [PLAN.md](./PLAN.md).
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
Two components, split over an HTTP boundary:
|
||||||
|
|
||||||
|
- **`dp` CLI** (Rust) — invoked by a thin per-editor keystroke shim. Resolves
|
||||||
|
git context, renders the selected lines to a PNG client-side, previews it in
|
||||||
|
the terminal, then `POST`s the card image + target board to the print service.
|
||||||
|
- **C4 print service** (Python / FastAPI + `brother_ql`) — a thin router that
|
||||||
|
forwards the raster to that board's networked Brother QL-1110NWB label
|
||||||
|
printer, and owns the board→printer registry, retries, and print status.
|
||||||
|
|
||||||
|
Target: capture → card in **one keystroke, ≤ 10 s**, for a fleet of ~400
|
||||||
|
engineers and 50+ boards. Rationale for every choice above lives in the
|
||||||
|
[ADRs](#decisions-adrs).
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
There is nothing to build or run yet. Once the two tracks in
|
||||||
|
[PLAN.md](./PLAN.md) land, install and usage instructions will live here — track
|
||||||
|
progress there in the meantime.
|
||||||
|
|
||||||
|
<!-- docs:start -->
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Read in this order — language first, then why, what, and how.
|
||||||
|
|
||||||
|
| Document | What it is |
|
||||||
|
| --- | --- |
|
||||||
|
| [CONTEXT.md](./CONTEXT.md) | **Ubiquitous language** — the project glossary. The shared vocabulary (Board, Defect, Defect card, yokoten…) that should appear verbatim in code, tests, commits, and docs. |
|
||||||
|
| [DESIGN.md](./DESIGN.md) | **Design (QFD)** — decomposes the goals into functions, approaches, components, and the tradeoffs taken, with strength matrices and the deployment context (~400 engineers, 50+ boards). |
|
||||||
|
| [SPEC.md](./SPEC.md) | **Spec (Phase 1)** — what we're building: the `dp` CLI (Part A) and the C4 print service (Part B), with the capture objective (one keystroke, ≤ 10 s) and the service interface. |
|
||||||
|
| [PLAN.md](./PLAN.md) | **Plan (Phase 2)** — how we'll build it: two parallel tracks (Rust `dp` CLI, Python print service) meeting at a single integration point, guided by *render first, print early*. |
|
||||||
|
|
||||||
|
### Decisions (ADRs)
|
||||||
|
|
||||||
|
Architectural decisions and the tradeoffs behind them — see [docs/adr/](./docs/adr).
|
||||||
|
|
||||||
|
| ADR | Decision |
|
||||||
|
| --- | --- |
|
||||||
|
| [0001](./docs/adr/0001-cli-engine-over-per-editor-extension.md) | CLI engine with per-editor keystroke shims, not a per-editor extension. |
|
||||||
|
| [0002](./docs/adr/0002-central-print-service-over-direct.md) | Central print service routing to networked label printers, not direct-to-printer or CUPS. |
|
||||||
|
| [0003](./docs/adr/0003-rust-plain-cli-not-tui.md) | `dp` CLI in Rust, as a plain command (not a TUI). |
|
||||||
|
| [0004](./docs/adr/0004-c4-service-in-python.md) | C4 print service in Python (FastAPI + `brother_ql`), not TypeScript or Gleam. |
|
||||||
|
<!-- docs:end -->
|
||||||
202
SPEC.md
Normal file
202
SPEC.md
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
# Spec: Dantotsu Print — `dp` CLI (v1)
|
||||||
|
|
||||||
|
> Phase 1 (Specify) artifact. Design rationale lives in [DESIGN.md](./DESIGN.md);
|
||||||
|
> vocabulary in [CONTEXT.md](./CONTEXT.md); decisions in [docs/adr/](./docs/adr).
|
||||||
|
> This spec covers **the `dp` CLI (Part A, most of this doc)** and the
|
||||||
|
> **C4 print-service (Part B, at the end)**.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Let a developer turn a code defect they're looking at into a **Defect card** —
|
||||||
|
a monochrome, peel-stick label sized to the ~90×85 mm Defect cell of a physical
|
||||||
|
Dantotsu board — in **one keystroke, ≤ 10 s**, without leaving the editor or
|
||||||
|
touching a screenshot/crop/print dance. The card shows the verbatim code with
|
||||||
|
the defect **visually marked**, plus a computer-native back-link to the exact
|
||||||
|
source. The tool exists to make filling the board *lower-friction than pasting
|
||||||
|
into Notion* (goal G1).
|
||||||
|
|
||||||
|
**Primary user:** a developer on a team that runs a daily Dantotsu board.
|
||||||
|
**Success = ** the capture step is no longer the reason people avoid the board.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Language:** Rust (stable), single static binary — **macOS, Linux, Windows**. See ADR-0003.
|
||||||
|
- **CLI framework:** `clap` (derive). No TUI framework (fire-and-exit).
|
||||||
|
- **Render:** `syntect` (TextMate grammars, `silicon`-style) → raster via the
|
||||||
|
`image` crate; **bundled mono font (default JetBrains Mono, SIL OFL),
|
||||||
|
ligatures OFF** (character-exact — see Open Q#1), rendered at **300 dpi** to
|
||||||
|
the **90×85 mm** cell.
|
||||||
|
- **Preview:** `viuer` (inline images: Kitty / iTerm2 / Ghostty / sixel), with a
|
||||||
|
text-grid and OS-viewer fallback.
|
||||||
|
- **Git:** shell out to `git blame`/`git rev-parse`/`git remote` (no libgit2
|
||||||
|
dependency in v1).
|
||||||
|
- **HTTP:** `reqwest` (blocking) to POST the card to the print service.
|
||||||
|
- **Auth:** Google Workspace SSO — OAuth 2.0 loopback/browser flow (`dp login`);
|
||||||
|
OIDC **Bearer** token cached (OS keychain, config fallback) and refreshed; the
|
||||||
|
service validates the `theodo.com` hosted domain.
|
||||||
|
- **QR (optional):** `qrcode` crate.
|
||||||
|
- **Config:** TOML via `serde`.
|
||||||
|
- Editor shims are **not** part of this crate — they are thin per-editor
|
||||||
|
snippets that invoke `dp` (see Project Structure → `shims/`).
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
**Dev / build (this repo):**
|
||||||
|
```
|
||||||
|
Build: cargo build --release
|
||||||
|
Test: cargo test
|
||||||
|
Lint: cargo clippy -- -D warnings
|
||||||
|
Format: cargo fmt
|
||||||
|
Run: cargo run -- <args>
|
||||||
|
```
|
||||||
|
|
||||||
|
**The `dp` surface (three commands):**
|
||||||
|
|
||||||
|
### `dp` — the hot path (capture → render → preview → emit)
|
||||||
|
```
|
||||||
|
… | dp --file <path> --mark <L:C-L:C>… [flags]
|
||||||
|
```
|
||||||
|
| Flag | Meaning | Default |
|
||||||
|
|------|---------|---------|
|
||||||
|
| `-f, --file <path>` | source file; enables git context + surrounding lines | — (else read stdin) |
|
||||||
|
| `--mark <L:C-L:C>` | emphasized defect span; **repeatable** (multi-cursor → many) | selection / whole `--lines` |
|
||||||
|
| `--lines <A-B>` | override the shown window | union of marks ± context |
|
||||||
|
| `--context <N>` | context lines around the marks | `3` |
|
||||||
|
| `--emphasis <box\|bold\|highlight\|underline>` | how spans are marked | `box` |
|
||||||
|
| `--board <id>` | target board | resolved from config |
|
||||||
|
| `--qr` | include the QR permalink | off |
|
||||||
|
| `--preview <image\|text\|os\|none>` | preview mode | `auto` (image if TTY supports, else text) |
|
||||||
|
| `-y, --yes` | skip preview + confirm | off |
|
||||||
|
| `--dry-run` | render + preview, do **not** emit | off |
|
||||||
|
| `--out <path>` | also write the PNG to disk | — |
|
||||||
|
| `--force` | print despite overflow (adds a visible truncation marker) | off |
|
||||||
|
|
||||||
|
**Flow:**
|
||||||
|
1. Resolve **board** (precedence: `--board` › repo `.dantotsu.toml` › user config). No board → abort with a hint to run `dp config`.
|
||||||
|
2. Acquire code: from `--file` (window = union of marks ± `--context`, or `--lines`), else from **stdin** (degraded: no context, no precise mark).
|
||||||
|
3. Derive **back-link**: `git blame -L` over the marked lines → *introducing* commit SHA (fallback `HEAD`); repo slug from the remote → footer `owner/repo · path · La–b · @sha`.
|
||||||
|
4. **Fit check** against the cell budget (~48 cols × ~19 rows @ 8 pt). Overflow → **abort**, reporting how far over and suggesting a narrower `--lines`; `--force` overrides with a truncation marker. Never silently shrink below the 8 pt floor.
|
||||||
|
5. **Render** the mono PNG (300 dpi, 90×85 mm): syntax-highlighted code, spans emphasized, footer band, optional QR, with the cell boundary drawn.
|
||||||
|
6. **Preview** unless `-y`: inline / text-grid / OS viewer; overflow visibly spills past the drawn boundary. Confirm `[y/N]`.
|
||||||
|
7. **Emit:** `POST {service}/print`. On success, print the resolved printer/board. On failure, exit non-zero with the service error. **No local queue in v1** (`--dry-run` to test without emitting).
|
||||||
|
|
||||||
|
### `dp config` — one-time per-dev setup
|
||||||
|
```
|
||||||
|
dp login # Google Workspace OAuth (browser); caches OIDC token
|
||||||
|
dp logout
|
||||||
|
dp config set service.url https://dantotsu.internal
|
||||||
|
dp config set default_board floor3-payments
|
||||||
|
dp config show
|
||||||
|
```
|
||||||
|
Writes `~/.config/dp/config.toml`; the OIDC token is cached in the OS keychain (config fallback). A committed repo-level `.dantotsu.toml` may set `board = "…"`.
|
||||||
|
|
||||||
|
### `dp doctor` — self-serve diagnostics (serves F4 at 50 boards)
|
||||||
|
Checks and reports pass/fail with hints: config present · service reachable · resolved `board → printer` (via `GET /boards/{id}`) · terminal graphics capability · `git` available · render font available.
|
||||||
|
|
||||||
|
**Print-service interface (the CLI depends on, C4 owns):**
|
||||||
|
- `POST /print` — multipart: `card` (image/png), `board` (string), `meta` (JSON: repo, path, lines, sha, ts). → `200 {printer, job_id}` or error.
|
||||||
|
- `GET /boards/{id}` — → `{printer, status}` (used by `dp doctor`).
|
||||||
|
- Auth: `Authorization: Bearer <Google OIDC id_token>`; the service validates audience + `hd=theodo.com`.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
main.rs → clap entry; dispatch dp / config / doctor
|
||||||
|
capture.rs → resolve file/stdin, marks, context window
|
||||||
|
gitctx.rs → blame → introducing SHA, repo slug, footer string
|
||||||
|
render/ → syntect highlight → mono PNG at cell size
|
||||||
|
layout.rs → cell budget, fit/overflow math, cell boundary
|
||||||
|
emphasis.rs → box/bold/highlight/underline over spans
|
||||||
|
footer.rs → back-link line + optional QR
|
||||||
|
preview.rs → viuer inline / text-grid / OS viewer
|
||||||
|
emit.rs → multipart POST to the print service
|
||||||
|
config.rs → TOML load/merge (flag › repo › user), board resolution
|
||||||
|
doctor.rs → diagnostics
|
||||||
|
tests/
|
||||||
|
fixtures/ → sample source files + committed golden PNGs
|
||||||
|
render_golden.rs → golden-image tests (tolerance-compared)
|
||||||
|
cli.rs → arg parsing, precedence, --dry-run against a mock service
|
||||||
|
shims/ → per-editor keystroke recipes (not part of the crate)
|
||||||
|
vim.md jetbrains.md vscode.md xcode.md
|
||||||
|
docs/adr/ → decisions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
Idiomatic Rust: `Result<T, DpError>` (via `thiserror`), no `unwrap()` outside tests, `snake_case`, doc-comments on public items, `clap` derive.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// A character-precise span to emphasize on the card: (line, col) → (line, col),
|
||||||
|
/// 1-based, inclusive start / exclusive end column. Supports multi-line spans.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Span {
|
||||||
|
pub start: (u32, u32),
|
||||||
|
pub end: (u32, u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Span {
|
||||||
|
/// Parse `L:C-L:C` (e.g. "108:12-110:4"). Column defaults to whole-line
|
||||||
|
/// when omitted (`108-110`), for editors that cannot report columns.
|
||||||
|
pub fn parse(s: &str) -> Result<Self, DpError> {
|
||||||
|
// … returns DpError::BadSpan(s.into()) on malformed input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
- **Framework:** `cargo test`. **Unit:** fit/overflow math, `Span::parse`, footer formatting, board-resolution precedence, blame-output parsing. **Integration:** `tests/cli.rs` drives the binary (arg parsing, precedence, `--dry-run` against a mock HTTP service); `tests/render_golden.rs` renders fixtures and compares to committed golden PNGs within a pixel tolerance.
|
||||||
|
- **Determinism:** pin font + `syntect` grammar/theme versions so golden images are stable; regenerating a golden requires explicit review.
|
||||||
|
- **Coverage:** meaningful coverage on `capture`/`render/layout`/`config` (the logic); no hard percentage gate.
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
- **Always:** run `cargo test` + `cargo clippy -D warnings` before commit; keep rendering deterministic for goldens; validate spans/line ranges before use; resolve an explicit board before emitting.
|
||||||
|
- **Ask first:** adding a heavy render dep (Skia, headless browser); changing the print-service API contract; changing the config schema or file locations; adding a new editor shim.
|
||||||
|
- **Never:** commit auth tokens or secrets; print to a board without an explicitly resolved board id; silently shrink code below the legible floor; regenerate golden images without review.
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
- **F1:** on a pilot board, selection → card-in-hand ≤ 10 s; hot path is one keystroke after selection (shim-bound).
|
||||||
|
- **F2:** card is byte-verbatim to the source; fits 90×85 mm; readable at ~0.4 m at 300 dpi; overflow **aborts** with an over-by message (no silent shrink).
|
||||||
|
- **F3:** preview renders the exact print artifact with the cell boundary and visible span marks; `-y` skips it.
|
||||||
|
- **F5:** the footer resolves to the introducing commit; `--qr` opens the correct commit-pinned permalink.
|
||||||
|
- **Ops (G2/F4):** `dp doctor` correctly diagnoses a mis-set board / unreachable service / unsupported terminal; the binary builds and runs on **macOS, Linux, and Windows** dev laptops (inline preview falls back to OS-viewer / text-grid on terminals without sixel — notably older Windows consoles).
|
||||||
|
- **Emit:** a successful run prints at the correct board's printer; failure exits non-zero with an actionable message.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
_(Both pilot-time confirmations — neither blocks implementation.)_
|
||||||
|
1. **Exact mono font** — default **JetBrains Mono, ligatures OFF** (ligatures would obscure operator defects, break sub-span marking, and need a shaping engine → net-negative for a defect card). Confirm legibility at 300 dpi / 8 pt on real prints (pins goldens), or swap the glyph set (FiraCode et al. are fine — just keep ligatures off).
|
||||||
|
2. **Removable adhesive** — label stock is in hand; confirm it's *removable* so peel-on-resolve doesn't tear the A3.
|
||||||
|
|
||||||
|
_Resolved: **C4 (print service) owner = the Dantotsu Print lead** — same owner as this CLI, so both sides of the `POST /print` / `GET /boards` + Google-OIDC contract are co-owned; auth = Google Workspace SSO (`dp login`, OIDC Bearer, `hd=theodo.com`); cell = 90×85 mm; mono confirmed; targets = macOS + Linux + Windows; label stock available._
|
||||||
|
|
||||||
|
## Explicit v1 Non-Goals
|
||||||
|
Interactive trim/markup (deferred ratatui/GUI mode) · gap-eliding `── snip ──` between distant disjoint spans · SSO short-link redirector · sub-line marking in VS Code/Cursor (needs the thin extension) · colour on paper · editor extensions beyond keystroke shims · non-git repos · local print queue/retry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Part B — C4 print-service (v1)
|
||||||
|
|
||||||
|
Small internal service (component **C4**) that receives cards and drives the board printers. Built here, owned by the project lead. Topology: ADR-0002 · language: ADR-0004.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Accept a rendered Defect card + a board id, and print it on that board's networked label printer — the thin router that lets the CLI stay client-side and lets 50+ printers be managed centrally.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
Python · FastAPI · `brother_ql` (PNG → QL raster over the network) · `google-auth` (OIDC verification). Containerized, deployed on an internal host.
|
||||||
|
|
||||||
|
## Endpoints (contract also consumed by the CLI, Part A)
|
||||||
|
- **`POST /print`** — auth: `Authorization: Bearer <Google OIDC id_token>` (validate audience + `hd=theodo.com`). Body multipart: `card` (image/png), `board` (id), `meta` (JSON: repo, path, lines, sha, ts). Resolves board → printer, converts PNG → QL raster (102 mm continuous, auto-cut), sends via `brother_ql`. → `200 {printer, job_id}` | `4xx/5xx {error}`.
|
||||||
|
- **`GET /boards/{id}`** — → `{printer, status}` (printer reachability / label state as far as the device reports). Backs `dp doctor`.
|
||||||
|
|
||||||
|
## Registry
|
||||||
|
A config file / small store mapping `board-id → {printer_ip, model, media}`, editable by the owner. No self-service registration in v1.
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
- **Always:** validate the OIDC token (audience + `hd`) before printing; reject unregistered boards.
|
||||||
|
- **Ask first:** changing the `/print` or `/boards` contract (it's shared with the CLI).
|
||||||
|
- **Never:** log card *contents* beyond metadata; expose the service unauthenticated.
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
A valid `POST /print` with a real board id prints the card on that board's QL-1110NWB; an unregistered board or an invalid/absent token is rejected with a clear error; `GET /boards/{id}` returns usable status for `dp doctor`.
|
||||||
|
|
||||||
|
## Non-Goals (v1)
|
||||||
|
Web UI · board self-registration · SSO short-link redirector (F5 later-upgrade) · multiple printers per board · retry/queue beyond one synchronous attempt.
|
||||||
|
|
||||||
18
docs/adr/0004-c4-service-in-python.md
Normal file
18
docs/adr/0004-c4-service-in-python.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# C4 print-service in Python (FastAPI + `brother_ql`), not TypeScript or Gleam
|
||||||
|
|
||||||
|
**Status:** accepted
|
||||||
|
|
||||||
|
The C4 print service is built in **Python (FastAPI)** using **`brother_ql`** to drive the networked Brother QL-1110NWB label printers. We reconsidered Node/Hono, Deno/Oak, and Gleam — the shop is TypeScript-heavy — but chose Python because the decisive part of C4 (converting the card PNG into the Brother QL raster protocol and sending it to the printer) has exactly one battle-tested implementation: `brother_ql` (Python). A TS/Gleam service would have to reimplement the QL raster protocol (real risk) or subprocess `brother_ql` anyway — which lands Python in the deployment regardless, stacking a second runtime on a three-endpoint service.
|
||||||
|
|
||||||
|
## Considered options
|
||||||
|
|
||||||
|
- **Python / FastAPI + `brother_ql` (chosen)** — native driver, one runtime, zero glue.
|
||||||
|
- **Node / Hono + `brother_ql` subprocess** — service logic in the team's language, but Python is still required for printing → two runtimes.
|
||||||
|
- **Deno / Oak** — as Node, with more friction for native/subprocess deps; no edge here.
|
||||||
|
- **Gleam** — no QL driver, immature OIDC; the same ecosystem gap that ruled it out for the CLI (ADR-0003).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- The stack is **Rust CLI + Python service**. The boundary is HTTP, so the split is clean; the CLI↔C4 contract (`POST /print`, `GET /boards`, Google OIDC) is unchanged.
|
||||||
|
- **TypeScript remains the right tool for the editor shims and any future UI** — this ADR is scoped to the printer-adjacent service only.
|
||||||
|
- Revisit if a mature non-Python QL raster driver ever appears.
|
||||||
Reference in New Issue
Block a user