ConusAI vendor v0.1.0-dev.20261027+ab5c16b3
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-dev.20261027+ab5c16b3
2026-10-27 16:27:40 +00:00
commit e2b9d1c8b0
538 changed files with 80203 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The types of scripts/agent-format.mjs, for the test that reads its rule.
export function fileToFormat(input: unknown, root?: string): string | null
+48
View File
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
//
// The format-on-edit hook of a coding agent (`.claude/settings.json`, PostToolUse on Edit and Write): it reads
// the hook's JSON from stdin and formats the one file the agent changed with this repository's Biome, so the
// gate reports real problems instead of whitespace. It never fails the agent's edit: a file Biome does not
// handle, or a missing install, is skipped silently.
//
// echo '{"tool_input":{"file_path":"modules/notes/core/x.ts"}}' | node scripts/agent-format.mjs
import { spawnSync } from "node:child_process"
import { existsSync } from "node:fs"
import { dirname, extname, join, relative, resolve } from "node:path"
import { fileURLToPath } from "node:url"
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..")
const FORMATTED = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json", ".jsonc", ".css"])
/** The repository-relative path the hook names, or null when there is nothing for Biome to format. */
export function fileToFormat(input, root = ROOT) {
const path = input?.tool_input?.file_path
if (typeof path !== "string" || path === "") return null
const absolute = resolve(root, path)
const rel = relative(root, absolute)
if (rel.startsWith("..") || rel.split(/[\\/]/).includes("node_modules")) return null
if (!FORMATTED.has(extname(rel)) || !existsSync(absolute)) return null
return rel
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
let text = ""
process.stdin.setEncoding("utf8")
process.stdin.on("data", (chunk) => {
text += chunk
})
process.stdin.on("end", () => {
let input = null
try {
input = JSON.parse(text)
} catch {
process.exit(0)
}
const file = fileToFormat(input)
const biome = join(ROOT, "node_modules", ".bin", "biome")
if (file === null || !existsSync(biome)) process.exit(0)
spawnSync(biome, ["format", "--write", "--no-errors-on-unmatched", file], { cwd: ROOT, stdio: "ignore" })
process.exit(0)
})
}
+54
View File
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The Convex profile's generator entry (spec §5.4): `pnpm convex:gen`. Rebuilds the module
// library (the registry is read from the manifests, not from a snapshot) and rewrites
// convex/schema.ts and convex/generated/<module>.ts. The output is deterministic: regeneration
// on an unchanged registry is byte-identical, and the profile gate asserts it.
import { mkdir, readdir, rm, writeFile } from "node:fs/promises"
import { buildRegistry } from "@conusai-kit/core"
import { writeConvexFiles } from "@conusai-kit/data-convex"
import { convexModules } from "../convex/lib/convexModules.mjs"
// The registry is read from the modules' built library (`pnpm run build:libs` first — the
// package.json script chains it), filtered to the modules the Convex runtime can deploy
// (convex/lib/convexModules.mjs, one exclusion map shared with the runtime).
import { appModules } from "../modules/dist/index.js"
const SPDX = "// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0\n"
// A module that declares its profiles and leaves out `convex` (knowledge, agent) is not deployed
// here: the runtime's module list (convex/lib/runtime.ts) and test/convex-profile.test.ts apply the
// same filter, so no wrapper is generated for a procedure the deployed registry does not hold.
const convexBound = appModules.filter((module) => module.profiles === undefined || module.profiles.includes("convex"))
const registry = buildRegistry([...convexModules(convexBound)])
const files = await writeConvexFiles(
registry,
new URL("../convex", import.meta.url).pathname,
{
mkdir,
writeFile,
},
{ header: SPDX },
)
const functionName = (name) => name.replace(/[^A-Za-z0-9_$]/g, "_")
const procedures = [...registry.procedures.values()]
.map(({ module, def }) => [def.name, { kind: def.kind, reference: `generated/${module}:${functionName(def.name)}` }])
.sort(([left], [right]) => left.localeCompare(right))
const procedureMap = `${SPDX}// Generated by @conusai-kit/data-convex (pnpm convex:gen). Do not edit by hand.\nexport const CONVEX_PROCEDURES = ${JSON.stringify(Object.fromEntries(procedures), null, 2)} as const\n`
await writeFile(new URL("../convex/generated/procedures.ts", import.meta.url), procedureMap)
// Convex bundles every exported .ts function it finds, whether or not another file imports it.
// Leaving a wrapper for an excluded module deploys a public endpoint whose runtime registry does
// not contain its procedure. Remove stale wrappers deterministically so generation is also the
// profile's deployment allowlist.
const expected = new Set([...files.map((file) => file.path.split("/").at(-1)), "procedures.ts"])
for (const entry of await readdir(new URL("../convex/generated/", import.meta.url), { withFileTypes: true })) {
if (entry.isFile() && entry.name.endsWith(".ts") && !expected.has(entry.name)) {
await rm(new URL(`../convex/generated/${entry.name}`, import.meta.url))
}
}
console.log(
`convex:gen: ${files.length + 1} file(s) written (${files
.map((f) => f.path)
.concat("generated/procedures.ts")
.join(", ")})`,
)
+105
View File
@@ -0,0 +1,105 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The layer rules of this repository, generated from the folder tree so a module added
// tomorrow is covered without anybody editing a list (app-template spec §2). Read by
// .dependency-cruiser.cjs and by test/boundaries.test.ts.
"use strict"
const fs = require("node:fs")
const path = require("node:path")
/** The module ids under modules/, which is the only place a domain module lives. */
function listModules(root) {
const dir = path.join(root, "modules")
if (!fs.existsSync(dir)) return []
return fs
.readdirSync(dir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort()
}
/** What a module's core may never reach: an engine, an SDK or a framework (spec §2). */
const DRIVERS_AND_FRAMEWORKS = [
"^(kysely|pg|pg-boss|mongodb|hono)$",
"^(react|react-dom|next)(/|$)",
"^@conusai-kit/(data-kysely|data-mongo|data-convex|jobs-pgboss|platform|ui|i18n|auth)(/|$)",
"^@conusai-sdk/",
]
function buildForbidden(moduleIds) {
const rules = [
{
name: "no-circular",
severity: "error",
comment: "A cycle between files makes the order they run in an accident. Move the shared part down a layer.",
from: {},
to: { circular: true },
},
{
name: "worker-entry-is-the-workers-own",
severity: "error",
comment:
"@conusai-kit/core/worker mints system actors. Only apps/worker may import it, so no transport can act as one (kit-core system-actor.ts).",
from: { pathNot: "^apps/worker/" },
// The resolved path, not the specifier: dependency-cruiser matches what a module resolves
// to, and in a pnpm store that is .../@conusai-kit/core/dist/worker.js.
to: { path: "@conusai-kit/core/dist/worker\\." },
},
{
name: "core-imports-no-engine-sdk-or-framework",
severity: "error",
comment:
"A use case under modules/<id>/core is pure TypeScript over the ports of @conusai-kit/core|data|tenancy, so it runs on every profile and needs no database to test (spec §2).",
from: { path: "^modules/[^/]+/core/" },
to: { path: DRIVERS_AND_FRAMEWORKS },
},
{
name: "contract-is-contract-only",
severity: "error",
comment:
"A module's contract/ is what other modules import, so it may not reach back into its own core, adapters or ui.",
from: { path: "^modules/([^/]+)/contract/" },
to: { path: "^modules/$1/(core|adapters|ui)/" },
},
{
name: "ui-talks-to-contracts-not-to-core",
severity: "error",
comment:
"A module's pages read and write through the data facade and its own contract, never its use cases (spec §6.3).",
from: { path: "^modules/([^/]+)/ui/" },
to: { path: "^modules/$1/(core|adapters)/" },
},
{
name: "a-module-never-imports-an-app",
severity: "error",
comment: "apps/* are composition roots: they import modules, never the other way round.",
from: { path: "^modules/" },
to: { path: "^apps/" },
},
{
name: "config-is-below-everything",
severity: "error",
comment: "config/ is read by apps and modules, so it may not import either of them.",
from: { path: "^config/" },
to: { path: "^(apps|modules)/" },
},
]
// One rule per module, from the folder tree: a module reaches another only through its
// contract/, and the message names both modules.
for (const id of moduleIds) {
const others = moduleIds.filter((other) => other !== id)
if (others.length === 0) continue
rules.push({
name: `module-${id}-crosses-only-contracts`,
severity: "error",
comment: `modules/${id} may import another module's contract/ and nothing else of it (spec §2, §4.4).`,
from: { path: `^modules/${id}/` },
to: { path: `^modules/(${others.join("|")})/(?!contract/)` },
})
}
return rules
}
module.exports = { DRIVERS_AND_FRAMEWORKS, buildForbidden, listModules }
+225
View File
@@ -0,0 +1,225 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
//
// `pnpm dev`: the whole app on this machine, against the fake platform, with demo data.
//
// 1. Postgres with the three roles a linked managed database exposes, in Docker (compose.dev.yaml),
// unless POSTGRES_URL, POSTGRES_READER_URL and POSTGRES_MIGRATE_URL are set in your shell
// 2. the fake platform of @conusai-kit/testing -- Blob, OCR, the AI gateway, audit, usage -- in this process
// 3. the libraries and the worker, built; then the migrate job, then the demo data (`pnpm seed`)
// 4. the worker, and the web shell under `next dev`
//
// It reads no file: a value you want different is a variable in your shell (`DEV_WEB_PORT=3200 pnpm dev`,
// `POSTGRES_URL=... pnpm dev`). The values it uses are printed. The fake platform is selected on purpose
// (CONUSAI_FAKE_PLATFORM=1) and the processes run with NODE_ENV=development, which is the only mode
// the app accepts that switch in (config/src/env.ts). Ctrl+C stops everything but Postgres, whose data
// stays; `docker compose -f compose.yaml -f compose.dev.yaml down` stops that too (add -v to drop the data).
import { spawn, spawnSync } from "node:child_process"
import { createHash, randomBytes } from "node:crypto"
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
import { basename, dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
import { startFakePlatform, validReply } from "@conusai-kit/testing/fake-platform"
const root = join(dirname(fileURLToPath(import.meta.url)), "..")
const say = (line) => console.log(`dev: ${line}`)
const port = (name, fallback) => {
const value = Number.parseInt(process.env[name] ?? "", 10)
return Number.isInteger(value) && value > 0 && value < 65_536 ? value : fallback
}
const ports = {
web: port("DEV_WEB_PORT", 3000),
platform: port("DEV_PLATFORM_PORT", 4010),
postgres: port("DEV_POSTGRES_PORT", 55_433),
}
/** The session secret, made once and kept (`.dev/` is ignored), so a restart does not sign everybody out. */
function sessionSecret() {
if (process.env.AUTH_SESSION_SECRET) return process.env.AUTH_SESSION_SECRET
const file = join(root, ".dev", "session-secret")
if (existsSync(file)) return readFileSync(file, "utf8").trim()
mkdirSync(dirname(file), { recursive: true })
const secret = randomBytes(32).toString("base64url")
writeFileSync(file, `${secret}\n`, { mode: 0o600 })
return secret
}
const url = (role) => `postgresql://appdev_${role}:appdev_${role}@127.0.0.1:${ports.postgres}/appdev`
const ownDatabase = ["POSTGRES_URL", "POSTGRES_READER_URL", "POSTGRES_MIGRATE_URL"].every((name) => process.env[name])
const platformUrl = `http://127.0.0.1:${ports.platform}`
const environment = {
...process.env,
NODE_ENV: "development",
CONUSAI_FAKE_PLATFORM: "1",
CONUSAI_API_URL: `${platformUrl}/api`,
CONUSAI_API_TOKEN: "dev-token",
CONUSAI_PROJECT_ID: "1",
CONUSAI_ORG_ID: "dev",
CONUSAI_PUBLIC_URL: `http://127.0.0.1:${ports.web}`,
CONUSAI_RELEASE: "dev",
POSTGRES_URL: process.env.POSTGRES_URL ?? url("app"),
POSTGRES_READER_URL: process.env.POSTGRES_READER_URL ?? url("reader"),
POSTGRES_MIGRATE_URL: process.env.POSTGRES_MIGRATE_URL ?? url("owner"),
AUTH_SESSION_SECRET: sessionSecret(),
AUTH_DEV_LOGIN: "1",
AUTH_INSECURE_COOKIES: "1",
DEV_POSTGRES_PORT: String(ports.postgres),
// The development Postgres is the pgvector image (compose.dev.yaml), so the knowledge module's
// vectors live there unless the shell chose otherwise.
KNOWLEDGE_VECTOR_STORE: process.env.KNOWLEDGE_VECTOR_STORE ?? "pgvector",
}
/** The environment of every process but the migrate job: the owner's URL is the migrate job's alone. */
const { POSTGRES_MIGRATE_URL: ownerUrl, ...withoutOwner } = environment
/** Runs a command to its end, printing what it prints; stops the run when it fails. */
function run(label, command, args, options = {}) {
say(label)
const result = spawnSync(command, args, { cwd: root, env: withoutOwner, stdio: "inherit", ...options })
if (result.status !== 0) {
console.error(`dev: ${label} failed${result.error ? `: ${result.error.message}` : ""}`)
process.exit(result.status ?? 1)
}
}
const compose = [
"compose",
"-p",
`${basename(root)}-dev-${createHash("sha1").update(root).digest("hex").slice(0, 6)}`,
"-f",
"compose.yaml",
"-f",
"compose.dev.yaml",
]
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
async function startPostgres() {
if (ownDatabase) {
say("using the Postgres your POSTGRES_URL, POSTGRES_READER_URL and POSTGRES_MIGRATE_URL name")
return
}
if (spawnSync("docker", ["info"], { stdio: "ignore" }).status !== 0) {
console.error(
"dev: Docker is not running, and Postgres is started in Docker. Start it, or set POSTGRES_URL, POSTGRES_READER_URL and POSTGRES_MIGRATE_URL to a database with the three roles of dev/postgres-roles.sql.",
)
process.exit(1)
}
run(`starting Postgres on 127.0.0.1:${ports.postgres}`, "docker", [...compose, "up", "-d", "--wait", "postgres"], {
env: environment, // compose interpolates every service of the file, the migrate job's owner URL included
})
// Healthy means the server answers; the roles are created by the init script, which restarts the
// server. "Ready" is the app's own login working.
for (let attempt = 0; ; attempt += 1) {
const probe = spawnSync(
"docker",
[
...compose,
"exec",
"-T",
"postgres",
"psql",
"-U",
"appdev_app",
"-h",
"127.0.0.1",
"-d",
"appdev",
"-c",
"select 1",
],
{ cwd: root, env: withoutOwner, stdio: "ignore" },
)
if (probe.status === 0) return
if (attempt > 120) {
console.error("dev: Postgres did not accept the app's login in a minute")
process.exit(1)
}
await sleep(500)
}
}
const children = []
function start(name, command, args, cwd) {
const child = spawn(command, args, { cwd, env: withoutOwner, detached: true, stdio: ["ignore", "pipe", "pipe"] })
for (const stream of [child.stdout, child.stderr]) {
let pending = ""
stream.on("data", (chunk) => {
pending += chunk.toString("utf8")
const lines = pending.split("\n")
pending = lines.pop() ?? ""
for (const line of lines) console.log(`[${name}] ${line}`)
})
}
child.on("exit", (code, signal) => {
if (!stopping) {
console.error(`dev: ${name} stopped (${signal ?? code}); stopping the rest`)
void stop(1)
}
})
children.push(child)
}
let stopping = false
let platform
async function stop(code = 0) {
if (stopping) return
stopping = true
for (const child of children) {
try {
if (child.pid !== undefined) process.kill(-child.pid, "SIGTERM")
} catch {
// already gone
}
}
await platform?.close().catch(() => undefined)
process.exit(code)
}
process.on("SIGINT", () => void stop(0))
process.on("SIGTERM", () => void stop(0))
await startPostgres()
platform = await startFakePlatform(ports.platform)
// Nothing scripts the gateway in a development run, so a structured request is answered with the
// smallest value its schema accepts and what is built on a model runs to its end.
platform.ai.respond(validReply)
say(`the fake platform is at ${platform.url}`)
run("building the libraries and the worker", "pnpm", ["run", "build:libs"])
run("building the worker", "pnpm", ["--filter", "@app/worker", "run", "build"])
run("migrating the database", "node", ["apps/worker/dist/migrate.js"], {
env: { ...withoutOwner, POSTGRES_MIGRATE_URL: ownerUrl },
})
run("writing the demo data", "node", ["apps/worker/dist/seed-main.js"])
start("worker", "node", ["apps/worker/dist/main.js"], root)
start(
"web",
process.execPath,
["node_modules/next/dist/bin/next", "dev", "-p", String(ports.web), "-H", "127.0.0.1"],
join(root, "apps", "web"),
)
for (let attempt = 0; ; attempt += 1) {
try {
if ((await fetch(`${environment.CONUSAI_PUBLIC_URL}/api/ready`)).ok) break
} catch {
// not yet
}
if (attempt > 240) {
console.error("dev: the web shell was not ready in two minutes")
await stop(1)
}
await sleep(500)
}
say("ready. Sign in as one of the demo people:")
for (const [name, email] of [
["Ada, owner of Acme Review and admin of Northwind", "ada@example.test"],
["Grace, owner of Northwind", "grace@example.test"],
["Linus, a viewer in Acme Review", "linus@example.test"],
]) {
say(` ${environment.CONUSAI_PUBLIC_URL}/auth/dev-login?email=${email} ${name}`)
}
say("Ctrl+C stops it.")
+14
View File
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The types of scripts/licence.mjs, for the tests that read its rules.
export const SPDX_ID: string
export const SPDX_LINE: string
export const LIST_FILE: string
export function headerFor(path: string): string | null
export function templateFiles(root: string): string[]
export function listText(paths: readonly string[]): string
export function parseList(text: string): string[]
export function hasHeader(text: string, path: string): boolean
export function withHeader(text: string, path: string): string
export function problems(root: string): string[]
export function write(root: string): string[]
+207
View File
@@ -0,0 +1,207 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
//
// The licence layout of this template (plan item AT5.1, spec section 14.3), as a tool and as the
// rules `kit check` and the tests read.
//
// node scripts/licence.mjs --check fail, naming each file, when the layout is not whole
// node scripts/licence.mjs --write add the missing headers and rewrite TEMPLATE-FILES
//
// What is the template's and what is the customer's:
//
// - Template files (this tool's `templateFiles`) carry the SPDX header `LicenseRef-ConusAI-Template-1.0`
// and are listed in TEMPLATE-FILES: every file of the scaffold except `modules/**` and `test/modules/**` (the
// customer's domain code and its app-level tests, which carry the customer's licence), `LICENSE` (the customer's own file, a stub
// until they fill it in), `pnpm-lock.yaml` (generated per bundle and then maintained by the
// customer) and `.conusai/template.json` (the upgrade record, written by the bundle).
// - A file that cannot hold a comment (JSON, an image, llms.txt) is listed and exempt from
// the header: `headerFor` answers null for it.
//
// `kit check` of @conusai-kit/cli reads the same two facts, TEMPLATE-FILES and the header, so a file a
// customer or an agent copies out of the scaffold without its header fails the gate.
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
import { dirname, join, relative, sep } from "node:path"
import { fileURLToPath } from "node:url"
export const SPDX_ID = "LicenseRef-ConusAI-Template-1.0"
export const SPDX_LINE = `SPDX-License-Identifier: ${SPDX_ID}`
export const LIST_FILE = "TEMPLATE-FILES"
/** Directories that are never part of the shipped template, at any depth. */
// `_generated`: Convex codegen output, written by a deploy and git-ignored.
const IGNORED_DIRS = new Set([
"node_modules",
".next",
"dist",
"test-results",
"playwright-report",
".logs",
".dev",
"_generated",
])
/** Files that are the customer's, or written by the bundle, and so are not the template's. */
const NOT_THE_TEMPLATES = new Set(["LICENSE", "pnpm-lock.yaml", ".conusai/template.json", ".env", ".env.local"])
const CUSTOMER_PREFIXES = ["modules/", "test/modules/"]
/** Files a tool writes: Next.js rewrites next-env.d.ts, TypeScript writes build info. They are not shipped. */
const GENERATED = (name) => name === "next-env.d.ts" || name.endsWith(".tsbuildinfo")
/** `//`-style comment types, by extension. */
const SLASH = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs"])
const HASH = new Set([".yml", ".yaml", ".sh", ".npmrc", ".gitignore", ".dockerignore", ".example"])
const HASH_NAMES = new Set(["Dockerfile", ".npmrc", ".gitignore", ".dockerignore"])
const extensionOf = (path) => {
const name = path.slice(path.lastIndexOf("/") + 1)
const dot = name.lastIndexOf(".")
return dot <= 0 ? "" : name.slice(dot)
}
/**
* The comment that carries the header in this file, or null when the file cannot hold one. Plain text
* the licence layout itself writes (LICENSE-TEMPLATE, NOTICE, THIRD-PARTY-NOTICES, TEMPLATE-FILES) states
* its licence in its own words, so it is exempt too.
*/
export function headerFor(path) {
const name = path.slice(path.lastIndexOf("/") + 1)
const ext = extensionOf(path)
if (SLASH.has(ext)) return `// ${SPDX_LINE}`
if (ext === ".css") return `/* ${SPDX_LINE} */`
if (ext === ".sql") return `-- ${SPDX_LINE}`
if (ext === ".md") return `<!-- ${SPDX_LINE} -->`
if (HASH.has(ext) || HASH_NAMES.has(name) || name.startsWith(".env")) return `# ${SPDX_LINE}`
return null
}
/** Every file the template ships that is the template's, as sorted POSIX paths relative to `root`. */
export function templateFiles(root) {
const found = []
const walk = (dir) => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!IGNORED_DIRS.has(entry.name)) walk(join(dir, entry.name))
} else if (entry.isFile() && !GENERATED(entry.name)) {
const rel = relative(root, join(dir, entry.name)).split(sep).join("/")
if (NOT_THE_TEMPLATES.has(rel) || CUSTOMER_PREFIXES.some((prefix) => rel.startsWith(prefix))) continue
found.push(rel)
}
}
}
walk(root)
return found.sort()
}
/** The text TEMPLATE-FILES holds for a set of paths. */
export function listText(paths) {
return [
"# The files of this template that ConusAI owns: the scaffold `kit template:upgrade` merges, and the",
`# files that carry the header \`${SPDX_ID}\` (LICENSE-TEMPLATE). One path per line.`,
"#",
"# Not listed, because they are yours: `modules/**` (your domain code), `test/modules/**` (its tests that",
"# need the whole app), `LICENSE` (your own licence), `pnpm-lock.yaml` (your own lock) and",
"# `.conusai/template.json` (the upgrade record). A file that",
"# cannot hold a comment (JSON, llms.txt) is listed and has no header. Regenerate with",
"# `node scripts/licence.mjs --write`.",
...paths,
"",
].join("\n")
}
/** The paths a TEMPLATE-FILES text lists. */
export function parseList(text) {
return text
.split("\n")
.map((line) => line.trim())
.filter((line) => line !== "" && !line.startsWith("#"))
}
/**
* The line the header sits on: after a shebang, and after the YAML frontmatter of a Markdown file that has one
* (an agent skill or command, `.claude/skills/<name>/SKILL.md`, must open with `---` for its tool to read it). A
* BOM or a directive's line is not skipped: the header leads. Keep frontmatter short: `kit check` looks for the
* header in a file's first eight lines.
*/
function headerLine(lines, path) {
if (lines[0]?.startsWith("#!")) return 1
if (extensionOf(path) === ".md" && lines[0]?.trim() === "---") {
const close = lines.indexOf("---", 1)
if (close > 0) return close + 1
}
return 0
}
/** Whether `text` carries the header on its header line (see `headerLine`). */
export function hasHeader(text, path) {
const header = headerFor(path)
if (header === null) return true
const lines = text.replace(/^/, "").split("\n")
return lines[headerLine(lines, path)]?.trim() === header
}
/** The text with the header added on its header line (see `headerLine`). */
export function withHeader(text, path) {
const header = headerFor(path)
if (header === null || hasHeader(text, path)) return text
const lines = text.split("\n")
lines.splice(headerLine(lines, path), 0, header)
return lines.join("\n")
}
/**
* Everything wrong with the licence layout under `root`, as one sentence each. Empty when whole.
*/
export function problems(root) {
const found = []
for (const required of ["LICENSE-TEMPLATE", "LICENSE", "NOTICE", LIST_FILE, "THIRD-PARTY-NOTICES"]) {
if (!existsSync(join(root, required))) found.push(`${required} is missing`)
}
const actual = templateFiles(root)
if (existsSync(join(root, LIST_FILE))) {
const listed = new Set(parseList(readFileSync(join(root, LIST_FILE), "utf8")))
for (const path of actual) if (!listed.has(path)) found.push(`${LIST_FILE} does not list ${path}`)
for (const path of listed)
if (!actual.includes(path)) found.push(`${LIST_FILE} lists ${path}, which is not a template file`)
}
for (const path of actual) {
const text = readFileSync(join(root, path), "utf8")
if (!hasHeader(text, path)) found.push(`${path} has no "${SPDX_LINE}" header`)
}
return found
}
/** Adds every missing header and rewrites TEMPLATE-FILES; answers the paths it changed. */
export function write(root) {
const changed = []
// The list first: it is itself one of the files.
const paths = [...new Set([...templateFiles(root), LIST_FILE])].sort()
writeFileSync(join(root, LIST_FILE), listText(paths))
for (const path of paths) {
const file = join(root, path)
if (!statSync(file).isFile()) continue
const before = readFileSync(file, "utf8")
const after = withHeader(before, path)
if (after !== before) {
writeFileSync(file, after)
changed.push(path)
}
}
return changed
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
const root = join(dirname(fileURLToPath(import.meta.url)), "..")
const mode = process.argv[2]
if (mode === "--write") {
const changed = write(root)
console.log(
`licence: ${changed.length} file(s) changed; ${LIST_FILE} lists ${parseList(readFileSync(join(root, LIST_FILE), "utf8")).length}`,
)
} else if (mode === "--check") {
const found = problems(root)
for (const problem of found) console.error(`licence: ${problem}`)
if (found.length > 0) process.exit(1)
console.log("licence: the layout is whole")
} else {
console.error("usage: node scripts/licence.mjs --check | --write")
process.exit(2)
}
}
+35
View File
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The types of scripts/sbom.mjs, for the tests that read its output.
export interface Component {
type: string
"bom-ref": string
group?: string
name: string
version: string
purl: string
licenses: Array<{
expression?: string
license?: { id?: string; name?: string; text?: { contentType: string; content: string } }
}>
properties?: Array<{ name: string; value: string }>
externalReferences?: Array<{ type: string; url: string }>
}
export interface Bom {
bomFormat: string
specVersion: string
version: number
metadata?: { component?: { type: string; name: string; version: string } }
components: Component[]
}
export function npmComponents(root: string): Component[]
export function dpkgComponents(statusFile: string, docRoot?: string): Component[]
export function sbom(options: {
root: string
dpkg?: string
name?: string
version?: string
nodeVersion?: string
}): Bom
+199
View File
@@ -0,0 +1,199 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
//
// The software bill of materials of a built image, as CycloneDX 1.5 JSON (plan item AT5.1). It reads
// what is installed, not what was asked for: every npm package in the image's pnpm store after
// `pnpm prune --prod` (so a runtime dependency and nothing else), the Node.js runtime, and the
// operating system packages of the base image. THIRD-PARTY-NOTICES is generated from its output
// (scripts/third-party-notices.mjs).
//
// docker run --rm --entrypoint node <image> scripts/sbom.mjs > sbom.cdx.json
// node scripts/sbom.mjs --root <dir> [--dpkg <status file>] [--name <app>] [--version <v>]
//
// No dependency: the script runs in the image, where only the runtime dependencies are installed.
import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs"
import { join } from "node:path"
import { fileURLToPath } from "node:url"
/** The most licence text kept per package. A licence longer than this is a file, not a notice. */
const MAX_LICENCE_BYTES = 64 * 1024
const LICENCE_FILE = /^(licen[cs]e|copying|unlicen[cs]e)(\.|-|$)/i
/** The ConusAI packages: their licence is LICENSE-TEMPLATE, whatever their package.json says. */
const CONUSAI_SCOPES = new Set(["@conusai-kit", "@conusai-sdk"])
const isRealDirectory = (path) => {
const stat = lstatSync(path, { throwIfNoEntry: false })
return stat?.isDirectory() === true && !stat.isSymbolicLink()
}
/** The `[name, dir]` of each package a pnpm virtual-store entry holds: real directories, not the links to its dependencies. */
function packagesInStoreEntry(entryNodeModules) {
const found = []
for (const name of readdirSync(entryNodeModules).sort()) {
const dir = join(entryNodeModules, name)
if (name.startsWith("@")) {
if (!isRealDirectory(dir)) continue
for (const inner of readdirSync(dir).sort()) {
if (isRealDirectory(join(dir, inner))) found.push(join(dir, inner))
}
} else if (isRealDirectory(dir)) {
found.push(dir)
}
}
return found
}
/** The licence a manifest declares, as a string, or null. */
function declaredLicence(manifest) {
const { license, licenses } = manifest
if (typeof license === "string" && license !== "") return license
if (license && typeof license === "object" && typeof license.type === "string") return license.type
if (Array.isArray(licenses)) {
const types = licenses.map((entry) => (typeof entry === "string" ? entry : entry?.type)).filter(Boolean)
if (types.length > 0) return types.length === 1 ? types[0] : `(${types.join(" OR ")})`
}
return null
}
function licenceText(dir) {
const file = readdirSync(dir)
.sort()
.find((name) => LICENCE_FILE.test(name) && lstatSync(join(dir, name)).isFile())
if (!file) return null
const bytes = readFileSync(join(dir, file))
if (bytes.length > MAX_LICENCE_BYTES) return null
return { file, text: bytes.toString("utf8").replace(/\r\n/g, "\n").trim() }
}
const repositoryUrl = (manifest) => {
const repository = manifest.repository
const url = typeof repository === "string" ? repository : repository?.url
return typeof url === "string" ? url.replace(/^git\+/, "").replace(/\.git$/, "") : null
}
const purlOf = (name, version) => `pkg:npm/${name.startsWith("@") ? `%40${name.slice(1)}` : name}@${version}`
/** One CycloneDX component for an installed npm package. */
function npmComponent(dir) {
const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"))
const { name, version } = manifest
if (typeof name !== "string" || typeof version !== "string") return null
const scope = name.startsWith("@") ? name.slice(0, name.indexOf("/")) : undefined
const declared = declaredLicence(manifest)
const evidence = licenceText(dir)
const conusai = scope !== undefined && CONUSAI_SCOPES.has(scope)
const component = {
type: "library",
"bom-ref": purlOf(name, version),
...(scope === undefined ? {} : { group: scope }),
name: scope === undefined ? name : name.slice(scope.length + 1),
version,
purl: purlOf(name, version),
licenses:
declared === null
? []
: /[\s()]/.test(declared)
? [{ expression: declared }]
: [
{
license: {
...(declared.startsWith("LicenseRef-") ? { name: declared } : { id: declared }),
...(evidence ? { text: { contentType: "text/plain", content: evidence.text } } : {}),
},
},
],
properties: [
...(conusai ? [{ name: "conusai:component", value: "true" }] : []),
...(evidence ? [{ name: "licence-file", value: evidence.file }] : []),
],
}
const url = repositoryUrl(manifest)
if (url) component.externalReferences = [{ type: "vcs", url }]
return component
}
/** Every npm package installed under `root`'s pnpm store, once per name@version, sorted. */
export function npmComponents(root) {
const store = join(root, "node_modules", ".pnpm")
const unique = new Map()
if (!existsSync(store)) return []
for (const entry of readdirSync(store).sort()) {
const nodeModules = join(store, entry, "node_modules")
if (!existsSync(nodeModules)) continue
for (const dir of packagesInStoreEntry(nodeModules)) {
if (!existsSync(join(dir, "package.json"))) continue
const component = npmComponent(dir)
if (component && !unique.has(component["bom-ref"])) unique.set(component["bom-ref"], component)
}
}
return [...unique.values()].sort((a, b) => a["bom-ref"].localeCompare(b["bom-ref"]))
}
/** The installed packages of a dpkg status file, as CycloneDX components. */
export function dpkgComponents(statusFile, docRoot = "/usr/share/doc") {
if (!existsSync(statusFile)) return []
const found = []
for (const stanza of readFileSync(statusFile, "utf8").split("\n\n")) {
const field = (name) => new RegExp(`^${name}: (.*)$`, "m").exec(stanza)?.[1]
const name = field("Package")
const version = field("Version")
if (!name || !version || !/install ok installed/.test(field("Status") ?? "")) continue
const copyright = join(docRoot, name, "copyright")
found.push({
type: "library",
"bom-ref": `pkg:deb/debian/${name}@${version}`,
name,
version,
purl: `pkg:deb/debian/${name}@${version}`,
licenses: [],
properties: [
{ name: "conusai:layer", value: "os" },
...(existsSync(copyright) ? [{ name: "licence-file", value: copyright }] : []),
],
})
}
return found.sort((a, b) => a["bom-ref"].localeCompare(b["bom-ref"]))
}
/** The CycloneDX document for the image whose application sits at `root`. */
export function sbom({
root,
dpkg = "/var/lib/dpkg/status",
name = "app",
version = "0.0.0",
nodeVersion = process.version,
}) {
const runtime = {
type: "platform",
"bom-ref": `pkg:generic/nodejs@${nodeVersion.replace(/^v/, "")}`,
name: "node",
version: nodeVersion.replace(/^v/, ""),
purl: `pkg:generic/nodejs@${nodeVersion.replace(/^v/, "")}`,
licenses: [{ license: { id: "MIT" } }],
properties: [{ name: "licence-file", value: "/usr/local/LICENSE" }],
}
return {
bomFormat: "CycloneDX",
specVersion: "1.5",
version: 1,
metadata: { component: { type: "application", name, version } },
components: [runtime, ...npmComponents(root), ...dpkgComponents(dpkg)],
}
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
const args = process.argv.slice(2)
const option = (flag, fallback) => {
const at = args.indexOf(flag)
return at === -1 ? fallback : args[at + 1]
}
const root = option("--root", process.cwd())
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"))
const document = sbom({
root,
dpkg: option("--dpkg", "/var/lib/dpkg/status"),
name: option("--name", manifest.name ?? "app"),
version: option("--version", manifest.version ?? "0.0.0"),
})
process.stdout.write(`${JSON.stringify(document, null, 2)}\n`)
}
+7
View File
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// The types of scripts/third-party-notices.mjs, for the tests that read its output.
import type { Bom } from "./sbom.mjs"
export function notices(document: Bom): string
export function unlisted(document: Bom, text: string, options?: { byName?: boolean }): string[]
+163
View File
@@ -0,0 +1,163 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
//
// THIRD-PARTY-NOTICES, generated from the software bill of materials of the built image (plan item
// AT5.1; scripts/sbom.mjs writes it). It lists every component the image runs, with its licence and,
// for an npm package, the text of the licence file the package ships, grouped so that one text that
// 200 packages share is printed once.
//
// node scripts/third-party-notices.mjs <sbom.cdx.json> [--out THIRD-PARTY-NOTICES]
// node scripts/third-party-notices.mjs <sbom.cdx.json> --check [--out THIRD-PARTY-NOTICES]
//
// `--check` fails when the file is not what the SBOM generates; `--complete` fails when a component of the
// SBOM is not listed by name, which is what "lists every runtime dependency of the built image" is
// checked against the bundle-specific lock and package versions used to build the image.
// The output is a pure function of the SBOM: no date, no host name, no order that depends on a walk.
import { createHash } from "node:crypto"
import { existsSync, readFileSync, writeFileSync } from "node:fs"
import { fileURLToPath } from "node:url"
const property = (component, name) => component.properties?.find((entry) => entry.name === name)?.value
const nameOf = (component) => (component.group ? `${component.group}/${component.name}` : component.name)
const labelOf = (component) => `${nameOf(component)}@${component.version}`
/** The licence a component declares, as one string. */
function licenceOf(component) {
const entry = component.licenses?.[0]
if (!entry) return "NOT DECLARED"
if (entry.expression) return entry.expression
return entry.license?.id ?? entry.license?.name ?? "NOT DECLARED"
}
const textOf = (component) => component.licenses?.[0]?.license?.text?.content ?? null
const sha = (text) => createHash("sha256").update(text).digest("hex")
const rule = "=".repeat(78)
/** The notices text for an SBOM document. */
export function notices(document) {
const components = document.components ?? []
const app = document.metadata?.component
const conusai = components.filter((component) => property(component, "conusai:component") === "true")
const os = components.filter((component) => property(component, "conusai:layer") === "os")
const runtime = components.filter((component) => component.type === "platform")
const npm = components.filter(
(component) => !conusai.includes(component) && !os.includes(component) && !runtime.includes(component),
)
const lines = [
"THIRD-PARTY NOTICES",
"",
`This file lists the software in the image ${app ? `${app.name} ${app.version} ` : ""}is built into, and the licence of each part.`,
"It is generated from the software bill of materials of the built image (CycloneDX, scripts/sbom.mjs) by",
"scripts/third-party-notices.mjs; do not edit it by hand. Regenerate it when the dependencies change:",
"",
" docker run --rm --entrypoint node <image> scripts/sbom.mjs > sbom.cdx.json",
" node scripts/third-party-notices.mjs sbom.cdx.json",
"",
`Components: ${components.length} (${conusai.length} ConusAI, ${npm.length} npm, ${runtime.length} runtime, ${os.length} operating system).`,
"",
rule,
`1. ConusAI components (${conusai.length})`,
rule,
"",
"Licensed under the ConusAI Template Licence 1.0 (LICENSE-TEMPLATE, still a placeholder) for the",
"@conusai-kit packages, and under the Apache License 2.0 for the @conusai-sdk packages.",
"",
...conusai.map((component) => ` ${labelOf(component)} (${licenceOf(component)})`),
"",
rule,
`2. Runtime (${runtime.length})`,
rule,
"",
...runtime.map(
(component) =>
` ${labelOf(component)} (${licenceOf(component)}); the licence text is /usr/local/LICENSE in the image`,
),
"",
rule,
`3. npm packages (${npm.length})`,
rule,
"",
]
// One group per licence text. A package that ships no licence file is grouped by what it declares.
const groups = new Map()
for (const component of npm) {
const text = textOf(component)
const declared = licenceOf(component)
const key = text === null ? `declared:${declared}` : `text:${sha(text)}`
if (!groups.has(key)) groups.set(key, { declared, text, members: [] })
groups.get(key).members.push(component)
}
const ordered = [...groups.values()].sort(
(a, b) => a.declared.localeCompare(b.declared) || labelOf(a.members[0]).localeCompare(labelOf(b.members[0])),
)
for (const group of ordered) {
lines.push(`Licence: ${group.declared}`, "Packages:")
for (const component of group.members) lines.push(` ${labelOf(component)}`)
lines.push("")
if (group.text === null) {
lines.push(` (these packages ship no licence file; they declare ${group.declared} in their package.json)`)
} else {
// Tabs are layout only in licence files. Expand them before adding our two-space prefix so
// generated notices stay readable and pass whitespace checks without changing licence text.
lines.push(...group.text.split("\n").map((line) => (line === "" ? "" : ` ${line.replaceAll("\t", " ")}`)))
}
lines.push("", "-".repeat(78), "")
}
lines.push(
rule,
`4. Operating system packages of the base image (${os.length})`,
rule,
"",
"Each package's licence is in its copyright file in the image, /usr/share/doc/<package>/copyright.",
"",
...os.map((component) => ` ${component.name} ${component.version}`),
"",
)
return lines.join("\n")
}
/**
* The components of an SBOM that a notices text does not list, by label. By name alone when `byName`
* is set: a version that moved since the file was written is drift to refresh, not a component missing.
*/
export function unlisted(document, text, { byName = false } = {}) {
return (document.components ?? [])
.filter((component) => {
if (property(component, "conusai:layer") === "os") {
return !text.includes(byName ? ` ${component.name} ` : ` ${component.name} ${component.version}\n`)
}
return !text.includes(byName ? ` ${nameOf(component)}@` : ` ${labelOf(component)}`)
})
.map(labelOf)
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
const args = process.argv.slice(2)
const flags = new Set(args.filter((arg) => arg.startsWith("--") && arg !== "--out"))
const outAt = args.indexOf("--out")
const out = outAt === -1 ? "THIRD-PARTY-NOTICES" : args[outAt + 1]
const input = args.find((arg, at) => !arg.startsWith("--") && (outAt === -1 || at !== outAt + 1))
if (!input) {
console.error("usage: node scripts/third-party-notices.mjs <sbom.cdx.json> [--check | --complete] [--out FILE]")
process.exit(2)
}
const document = JSON.parse(readFileSync(input, "utf8"))
const text = notices(document)
if (flags.has("--check") || flags.has("--complete")) {
const present = existsSync(out) ? readFileSync(out, "utf8") : ""
const missing = unlisted(document, present, { byName: flags.has("--complete") })
for (const label of missing) console.error(`third-party-notices: ${out} does not list ${label}`)
if (flags.has("--check") && present !== text && missing.length === 0) {
console.error(`third-party-notices: ${out} is not what the SBOM generates`)
}
process.exit(missing.length === 0 && (flags.has("--complete") || present === text) ? 0 : 1)
}
writeFileSync(out, text)
console.log(`third-party-notices: wrote ${out} (${document.components?.length ?? 0} components)`)
}
+39
View File
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Read-only browser smoke for a built image or a live deployment. No account is needed.
// pnpm exec playwright install chromium
// node scripts/verify-shell.mjs https://your-app.example.test
import { strict as assert } from "node:assert"
import { chromium, expect } from "@playwright/test"
const base = new URL(process.argv[2] ?? "http://127.0.0.1:3000")
assert(["http:", "https:"].includes(base.protocol), "expected an HTTP(S) app URL")
const browser = await chromium.launch()
try {
const context = await browser.newContext({ locale: "en-US" })
const page = await context.newPage()
page.setDefaultTimeout(15_000)
const errors = []
page.on("pageerror", (error) => errors.push(error.message))
const response = await page.goto(new URL("/", base).href, { waitUntil: "networkidle" })
assert.equal(response?.status(), 200)
const root = page.locator("html")
await page.getByRole("button", { name: "Theme: Dark", exact: true }).click()
await expect(root).toHaveClass("light")
await page.reload()
await expect(root).toHaveClass("light")
await page.getByRole("combobox", { name: /^Language/ }).selectOption("lt")
await expect(root).toHaveAttribute("lang", "lt")
await expect(page.getByRole("button", { name: "Tema: Šviesi", exact: true })).toBeVisible()
await page.reload()
await expect(root).toHaveAttribute("lang", "lt")
await expect(root).toHaveClass("light")
await page.getByRole("button", { name: "Tema: Šviesi", exact: true }).click()
await expect(root).not.toHaveClass("light")
await page.getByRole("combobox", { name: /^Kalba/ }).selectOption("en")
await expect(root).toHaveAttribute("lang", "en")
await expect(page.getByRole("button", { name: "Theme: Dark", exact: true })).toBeVisible()
assert.deepEqual(errors, [], "browser runtime errors")
console.log("verify-shell: theme and language switch both ways and persist across reloads")
} finally {
await browser.close()
}