ci: add quality gates (semgrep, lint, e2e smoke, advisory PR-Agent) #1
@@ -10,6 +10,35 @@ export default tseslint.config(
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
{
|
||||
rules: {
|
||||
// Leading underscore marks an intentionally unused binding (omitting a key via rest, unused
|
||||
// override parameters).
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_", ignoreRestSiblings: true },
|
||||
],
|
||||
// The BOM and NBSP are written literally on purpose in the CSV exporters and the text sanitiser.
|
||||
"no-irregular-whitespace": ["error", { skipStrings: true, skipTemplates: true, skipRegExps: true }],
|
||||
},
|
||||
},
|
||||
{
|
||||
// The React Compiler rules shipped in eslint-plugin-react-hooks 7 flag long-standing patterns
|
||||
// here (load data on mount, "latest value" refs written during render). They are legitimate
|
||||
// design feedback but not bugs, and rewriting them blind risks behaviour changes in an
|
||||
// effect-heavy app, so they stay visible as warnings until each site is reworked.
|
||||
files: ["src/**/*.{ts,tsx}"],
|
||||
rules: {
|
||||
"react-hooks/set-state-in-effect": "warn",
|
||||
"react-hooks/refs": "warn",
|
||||
"react-hooks/immutability": "warn",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test helper that reads react-pdf's untyped internal layout data.
|
||||
files: ["src/pdf/testing/**/*.ts"],
|
||||
rules: { "@typescript-eslint/no-explicit-any": "warn" },
|
||||
},
|
||||
{
|
||||
files: ["src/**/*.{ts,tsx}"],
|
||||
languageOptions: { globals: globals.browser },
|
||||
|
||||
@@ -52,7 +52,6 @@ function wordCommands(font) {
|
||||
function rotate(cmds) {
|
||||
const pts = cmds.flatMap((c) => c.args);
|
||||
const xs = pts.filter((_, i) => i % 2 === 0);
|
||||
const ys = pts.filter((_, i) => i % 2 === 1);
|
||||
const inkLen = Math.max(...xs) - Math.min(...xs);
|
||||
const s = INK_LENGTH / inkLen;
|
||||
const map = (x, y) => [-y * s, -x * s];
|
||||
|
||||
@@ -102,7 +102,6 @@ export async function runE2eSteps(log: StepLog, reportPath: string): Promise<voi
|
||||
},
|
||||
);
|
||||
|
||||
let inv!: Invoice;
|
||||
const issued = await log.run("e2e: issue and archive", async () => {
|
||||
const outcome = await issueAndArchive(input, prefs, deps);
|
||||
invoice = outcome.invoice;
|
||||
@@ -112,7 +111,7 @@ export async function runE2eSteps(log: StepLog, reportPath: string): Promise<voi
|
||||
};
|
||||
});
|
||||
if (!issued.ok || !invoice) return;
|
||||
inv = invoice as Invoice;
|
||||
const inv = invoice as Invoice;
|
||||
|
||||
await log.run("e2e: archive status", async () => {
|
||||
const st = await api.archiveStatus(inv.id);
|
||||
|
||||
@@ -22,6 +22,7 @@ export function sanitize(value: string | null | undefined): string {
|
||||
.replace(/\r\n?/g, "\n")
|
||||
.replace(/[\t\n\v\f]/g, " ")
|
||||
.replace(/ /g, " ")
|
||||
// eslint-disable-next-line no-control-regex -- stripping control characters is the point
|
||||
.replace(/[\u0000-\u001F\u007F-\u009F]/g, "")
|
||||
.replace(/ {2,}/g, " ")
|
||||
.trim();
|
||||
|
||||
@@ -21,7 +21,7 @@ import { RenderTxt } from "./blocks/RenderTxt";
|
||||
import { TableGuard, guardPresence } from "./blocks/TableGuard";
|
||||
import { renderCore } from "./render/core";
|
||||
import { loadCanvas, inkPixels } from "./testing/rasterize";
|
||||
import { byId, doc, FAMILY, find, frameFor, PAD, PAGE_W, rawLayout, txt } from "./testing/docs";
|
||||
import { byId, doc, FAMILY, find, frameFor, PAD, rawLayout, txt } from "./testing/docs";
|
||||
import { FONTS_DIR, setupPdfTest } from "./testing/setup";
|
||||
import { stack } from "./fonts/register";
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ export const REVERSE_GAP = 4;
|
||||
export const HEADING_AFTER = 4;
|
||||
/** Space between the closing block's two columns. */
|
||||
export const CLOSING_COLUMN_GAP = 16;
|
||||
/** Slate: space between a mark and the business name in the dark band. */
|
||||
const NAME_AFTER_LOGO = 7.8;
|
||||
/** Space above and below the thin rule that separates the stack from TOTAL. */
|
||||
export const STACK_RULE = { before: 2, after: 8, thickness: 0.75 };
|
||||
const MIN_NAME_W = 40;
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AuditIssue } from "../engine/audit";
|
||||
import { PAGE_SIZES } from "../engine/geometry";
|
||||
import { PAGE_LABEL_ID, TABLE_HEAD_ID, TABLE_ROW_PREFIX, CLOSING_ID, TOTALS_ID } from "../engine/ids";
|
||||
import { walk, type LayoutNode, type LayoutPage, type Rect } from "../engine/layoutTree";
|
||||
import { forbiddenProbe, SLOT_RULES, slotContent, type SlotName } from "../model/slots";
|
||||
import { frameForPrefs, renderInvoicePdf } from "../render/invoice";
|
||||
import { loadCanvas, pagePng } from "../testing/rasterize";
|
||||
import { SLOT_ID } from "../blocks/slots";
|
||||
import { META_ID, SUPPLIER_ID, TITLE_ID } from "./classic/ids";
|
||||
import { HEADER_GAP, headerCols } from "./classic/plan";
|
||||
import { FIXTURE_GSTIN, FIXTURES, buildFixture, ensureSetup, makeItems, PAGE_SIZE_IDS, prefsFor, renderFixture, type FixtureSpec, type Rendered } from "./harness";
|
||||
import { renderInvoicePdf } from "../render/invoice";
|
||||
import { FIXTURES, buildFixture, ensureSetup, PAGE_SIZE_IDS, prefsFor, renderFixture, type FixtureSpec } from "./harness";
|
||||
import { TEMPLATES } from "./registry";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Image, View } from "@react-pdf/renderer";
|
||||
import type { ReactNode } from "react";
|
||||
import { colsDetailed, pinned } from "../../engine/columns";
|
||||
import { pinned } from "../../engine/columns";
|
||||
import { CLOSING_ID, TABLE_HEAD_ID, TABLE_ROW_PREFIX, TOTALS_ID } from "../../engine/ids";
|
||||
import { AvailableWidthProvider } from "../../blocks/AvailableWidth";
|
||||
import { BottomSpacer } from "../../blocks/BottomSpacer";
|
||||
|
||||
@@ -202,7 +202,7 @@ function Parties({ ctx }: { ctx: Ctx }) {
|
||||
const blocks: ReactNode[] = [<ClientBlock key="a" ctx={ctx} width={text[0]} />];
|
||||
let k = 1;
|
||||
if (plan.info.hasBank) blocks.push(<PayableBlock key="b" ctx={ctx} width={text[k++]} />);
|
||||
if (plan.info.hasIds) blocks.push(<IdsBlock key="c" ctx={ctx} width={text[k++]} />);
|
||||
if (plan.info.hasIds) blocks.push(<IdsBlock key="c" ctx={ctx} width={text[k]} />);
|
||||
const specs: ColSpec[] = cols.map((w, i) => (i === cols.length - 1 ? { fill: true } : { fixed: w }));
|
||||
return (
|
||||
<View style={{ ...pinned(plan.W), paddingLeft: v.inset.left } as never}>
|
||||
|
||||
@@ -151,7 +151,7 @@ export function planMarble(model: RenderModel, frame: PageFrame, v: MarbleVarian
|
||||
let k = 0;
|
||||
const clientW = textW(k++);
|
||||
const bankW = hasBank ? textW(k++) : 0;
|
||||
const idsW = hasIds ? textW(k++) : 0;
|
||||
const idsW = hasIds ? textW(k) : 0;
|
||||
const head = leading(r.heading) + v.parties.headingGap;
|
||||
let clientH = head + textHeight(r.body, c.name, clientW);
|
||||
for (const l of c.addressLines) clientH += textHeight(r.body, l, clientW);
|
||||
|
||||
@@ -296,7 +296,7 @@ export function planNeutral(model: RenderModel, frame: PageFrame, v: NeutralVari
|
||||
for (const l of lines) payH += textHeight(r.address, l, payW);
|
||||
}
|
||||
if (v.id === "serenity") signH += leading(r.label) + 4;
|
||||
let closingH = 0;
|
||||
let closingH: number;
|
||||
if (v.payTo.where === "closing") closingH = Math.max(payH, signH);
|
||||
else closingH = signH;
|
||||
if (model.cancelled) closingH += 10 + textHeight(r.small, CANCELLED_SENTENCE, W);
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import type { StepResult } from "./lib/selfTestSteps";
|
||||
const WATCHDOG_MS = 120_000;
|
||||
|
||||
export async function maybeRunSelfTest(): Promise<void> {
|
||||
let reportPath: string | null = null;
|
||||
let reportPath: string | null;
|
||||
try {
|
||||
reportPath = await invoke<string | null>("selftest_config");
|
||||
} catch {
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
TabPanels,
|
||||
Tabs,
|
||||
Tile,
|
||||
Toggle,
|
||||
} from "@carbon/react";
|
||||
import { Add, Save, TrashCan } from "@carbon/icons-react";
|
||||
import { confirm } from "@tauri-apps/plugin-dialog";
|
||||
|
||||
@@ -48,8 +48,7 @@ import type {
|
||||
Settings,
|
||||
TaxType,
|
||||
} from "../lib/types";
|
||||
import { computeLineAmount, computeTotals } from "../lib/invoice";
|
||||
import { amountInWords } from "../lib/numberToWords";
|
||||
import { computeLineAmount } from "../lib/invoice";
|
||||
import { gstinFullError } from "../lib/validators";
|
||||
import { addDays, formatAmount, todayIso } from "../lib/format";
|
||||
import { modelFromAssets, prefsForInvoice } from "../lib/pdf";
|
||||
@@ -256,11 +255,6 @@ export default function NewInvoice({ settings, onSettingsChange, active, onActiv
|
||||
? "cgst_sgst"
|
||||
: "igst";
|
||||
const clientGstinError = gstinFullError(clientGstin);
|
||||
const totals = useMemo(
|
||||
() => computeTotals(items, discount, effectiveTaxType, taxRate),
|
||||
[items, discount, effectiveTaxType, taxRate],
|
||||
);
|
||||
const words = useMemo(() => amountInWords(totals.total), [totals.total]);
|
||||
const bank = useMemo(() => banks.find((b) => b.id === bankId) ?? null, [banks, bankId]);
|
||||
|
||||
const payload = useMemo(
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function SeriesSettings({
|
||||
embedded?: boolean;
|
||||
}) {
|
||||
const toast = useToast();
|
||||
const [active, setActive] = useState<InvoiceSeries | null>(null);
|
||||
const [, setActive] = useState<InvoiceSeries | null>(null);
|
||||
const [all, setAll] = useState<InvoiceSeries[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user