// Lint that a product's compose actually DECLARES the product to Studio. // // Studio names a product to the engine at the V2 licence handshake. Given no // NORSK_PRODUCT_NAME it falls back to its baked default, "norsk-studio", and so // demands a Studio entitlement rather than the product's own. A repo whose // licence is cut for its own product then launches instances that are refused: // Studio still serves its API, but every media node is refused, so SRT // listeners never bind and the graph produces nothing. // // That failure is nearly unreadable from outside. It is not a crash and not a // timeout at launch — the instance comes up "healthy" and only the media- // dependent assertions fail, which reads as a broken workflow rather than a // missing declaration. funke-pegasus shipped mis-declared from the day it was // created and only surfaced when a licence re-mint stopped carrying a // norsk-studio entry; reuters had declared its own name since 2026-07-28. // // check-template.ts is this lint's other half: it asserts the repo RECORDS the // entitlement it needs, and this asserts the repo ASKS for the one it records. // Either alone leaves the pair free to drift, which is exactly what happened. import { readFileSync } from "node:fs"; /** Source with comments blanked out, so a mention of the key in prose cannot * satisfy the lint. Walks the text rather than regex-replacing, so a `//` * inside a string literal (a URL) is not mistaken for a comment. */ export function stripComments(source: string): string { let out = ""; let i = 0; while (i < source.length) { const c = source[i]; const next = source[i + 1]; if (c === "/" && next === "/") { while (i < source.length && source[i] !== "\n") i++; continue; } if (c === "/" && next === "*") { i += 2; while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i++; i += 2; continue; } if (c === '"' || c === "'" || c === "`") { const quote = c; out += c; i++; while (i < source.length && source[i] !== quote) { if (source[i] === "\\") { out += source[i]; i++; } if (i < source.length) { out += source[i]; i++; } } out += quote; i++; continue; } out += c; i++; } return out; } const DECLARATION = /NORSK_PRODUCT_NAME\s*[:=]\s*(PRODUCT_NAME\b|"([^"]*)"|'([^']*)')/; /** `templateSource` is the repo's product-template module — the one that builds * the studio service's environment. `product` is the repo's PRODUCT_NAME. */ export function productNameProblems(templateSource: string, product: string): string[] { const match = DECLARATION.exec(stripComments(templateSource)); if (!match) { return [ `the studio service does not declare NORSK_PRODUCT_NAME, so Studio will ask for "norsk-studio" at the licence handshake instead of "${product}" — set NORSK_PRODUCT_NAME: PRODUCT_NAME in its environment`, ]; } const declared = match[2] ?? match[3]; // The PRODUCT_NAME constant is correct by construction; only a literal can // name the wrong thing. if (declared !== undefined && declared !== product) { return [`NORSK_PRODUCT_NAME is declared as "${declared}" but this repo's product is "${product}"`]; } return []; } if (import.meta.main) { const [file, product] = process.argv.slice(2); if (!file || !product) { console.error("usage: check-product-name.ts "); process.exit(2); } let contents: string; try { contents = readFileSync(file, "utf8"); } catch { console.error(`product-name: cannot read ${file}`); process.exit(1); } const problems = productNameProblems(contents, product); if (problems.length) { for (const p of problems) console.error(`product-name: ${p}`); process.exit(1); } console.log(`product-name: ${file} declares ${product} to Studio`); }