feat(linking): explicit [#42] card references

This commit is contained in:
Dave 2026-09-16 13:20:56 +02:00
parent d06d1e99ec
commit 7970edf7c2
2 changed files with 77 additions and 0 deletions

View file

@ -0,0 +1,50 @@
import { describe, it, expect } from "vitest";
import { extractCardReference } from "@/lib/linking/reference";
describe("extractCardReference", () => {
it("finds a bracketed reference at the end of the subject", () => {
expect(extractCardReference("feat(stock): inline qty editing [#42]")).toBe(42);
});
it("finds one anywhere in the message, including the body", () => {
expect(extractCardReference("[#7] fix: thing")).toBe(7);
expect(extractCardReference("fix: thing\n\nRefs [#7]\n")).toBe(7);
});
it("returns null when there is none", () => {
expect(extractCardReference("feat(stock): inline qty editing")).toBeNull();
expect(extractCardReference("")).toBeNull();
});
it("ignores a bare #42 without brackets", () => {
// Bare hashes appear constantly in prose — colours, issue numbers from
// other systems, ordinals. Requiring brackets makes the reference a
// deliberate act rather than a guess.
expect(extractCardReference("fix: matches #42 in the old tracker")).toBeNull();
expect(extractCardReference("style: use #ff8a5c")).toBeNull();
});
it("takes the first reference when several appear", () => {
// One commit links to at most one card, so a second reference cannot be
// honoured. First wins, deterministically.
expect(extractCardReference("fix: thing [#3] and also [#9]")).toBe(3);
});
it("rejects zero and negative numbers", () => {
expect(extractCardReference("fix: thing [#0]")).toBeNull();
expect(extractCardReference("fix: thing [#-4]")).toBeNull();
});
it("rejects a non-numeric reference", () => {
expect(extractCardReference("fix: thing [#abc]")).toBeNull();
expect(extractCardReference("fix: thing [#]")).toBeNull();
});
it("tolerates whitespace inside the brackets", () => {
expect(extractCardReference("fix: thing [# 42 ]")).toBe(42);
});
it("rejects an absurdly large number rather than overflowing", () => {
expect(extractCardReference(`fix: thing [#${"9".repeat(20)}]`)).toBeNull();
});
});

View file

@ -0,0 +1,27 @@
/**
* Explicit card references in commit messages: `[#42]`.
*
* Brackets are required. A bare `#42` appears far too often in ordinary prose —
* hex colours, issue numbers from other trackers, ordinals — and a linker that
* guesses at those attaches work to the wrong card, which is worse than not
* linking it at all.
*
* This is the override, not the daily habit: scope matching carries most
* commits. It exists for the times auto-linking gets it wrong.
*/
/** Above this, the value is not a card number; it is a typo or an overflow. */
const MAX_CARD_NUMBER = 1_000_000;
const REFERENCE = /\[#\s*(\d+)\s*\]/;
export function extractCardReference(message: string): number | null {
const match = REFERENCE.exec(message ?? "");
if (!match) return null;
const value = Number(match[1]);
if (!Number.isInteger(value)) return null;
if (value < 1 || value > MAX_CARD_NUMBER) return null;
return value;
}