paqad-ai
Version:
Spec-driven development framework — AI agents that think before they type
2,049 lines • 68.4 kB
JavaScript
import { createRequire as __paqadCreateRequire } from 'module'; const require = __paqadCreateRequire(import.meta.url);
import {
StackSnapshotCache
} from "./chunk-WRHJCBRI.js";
import {
buildDetectedStackProfile
} from "./chunk-HNYZUZ7D.js";
import {
DEFAULT_IGNORE_GLOBS,
dropGitIgnored,
scanWorkingTree
} from "./chunk-LBNBI3D6.js";
import {
require_out
} from "./chunk-P4RPU2KB.js";
import {
toPosixPath
} from "./chunk-LYNDVGNE.js";
import {
layeredConfigMap,
readTextIfExistsSync,
require_dist
} from "./chunk-ACJQW7SB.js";
import {
renameWithRetrySync
} from "./chunk-Z3NGWHFX.js";
import {
__toESM,
init_esm_shims
} from "./chunk-IDFRWBMN.js";
// src/rules-discovery/nudge.ts
init_esm_shims();
import { createHash } from "crypto";
import { readFileSync as readFileSync3 } from "fs";
import { basename as basename3, join as join9 } from "path";
// src/introspection/ecosystems/registry.ts
init_esm_shims();
var import_fast_glob = __toESM(require_out(), 1);
// src/introspection/ecosystems/shared.ts
init_esm_shims();
import { readFile } from "fs/promises";
import { join } from "path";
async function readProjectFile(projectRoot, relativePath) {
try {
return await readFile(join(projectRoot, relativePath), "utf8");
} catch {
return null;
}
}
function readJson(content) {
try {
return JSON.parse(content);
} catch {
return null;
}
}
function normalizeConstraint(value) {
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
if (value && typeof value === "object") {
const record = value;
if (typeof record.path === "string") {
return `path:${record.path}`;
}
if (typeof record.git === "string" || record.git && typeof record.git === "object") {
const git = record.git;
return `git:${typeof git === "string" ? git : String(git.url ?? "")}`;
}
if (typeof record.sdk === "string") {
return `sdk:${record.sdk}`;
}
if (typeof record.version === "string") {
return record.version;
}
}
return "unknown";
}
function parseKeyValueLines(content, separator) {
return content.split("\n").map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("#")).map((line) => {
const index = line.indexOf(separator);
if (index === -1) {
return null;
}
const name = line.slice(0, index).trim();
const value = line.slice(index + separator.length).trim();
if (name === "" || value === "") {
return null;
}
return { name, value };
}).filter((entry) => entry !== null);
}
// src/introspection/ecosystems/dart.ts
init_esm_shims();
var import_yaml = __toESM(require_dist(), 1);
var dartParser = {
ecosystem: "dart",
packageManager: "pub",
manifestFiles: ["pubspec.yaml"],
lockfileFiles: ["pubspec.lock"],
parseManifest(content) {
let parsed;
try {
parsed = import_yaml.default.parse(content);
} catch {
parsed = null;
}
return {
ecosystem: "dart",
packages: [
...Object.entries(parsed?.dependencies ?? {}).map(([name, value]) => ({
name,
constraint: normalizeConstraint(value),
isDev: false
})),
...Object.entries(parsed?.dev_dependencies ?? {}).map(([name, value]) => ({
name,
constraint: normalizeConstraint(value),
isDev: true
}))
].sort((left, right) => left.name.localeCompare(right.name))
};
},
parseLockfile(content) {
let parsed;
try {
parsed = import_yaml.default.parse(content);
} catch {
parsed = null;
}
return {
ecosystem: "dart",
packages: Object.entries(parsed?.packages ?? {}).filter(([, value]) => typeof value.version === "string").map(([name, value]) => ({ name, version: value.version })).sort((left, right) => left.name.localeCompare(right.name))
};
}
};
// src/introspection/ecosystems/dotnet.ts
init_esm_shims();
var dotnetParser = {
ecosystem: "dotnet",
packageManager: "nuget",
manifestFiles: ["*.csproj"],
lockfileFiles: ["packages.lock.json"],
parseManifest(content) {
return {
ecosystem: "dotnet",
packages: Array.from(
content.matchAll(/<PackageReference\b([^>]*?)(\/>|>([\s\S]*?)<\/PackageReference>)/g)
).map((match) => {
const attributes = match[1] ?? "";
const name = /\bInclude\s*=\s*"([^"]+)"/.exec(attributes)?.[1];
if (!name) {
return null;
}
const version = /\bVersion\s*=\s*"([^"]+)"/.exec(attributes)?.[1] ?? /<Version>([^<]+)<\/Version>/.exec(match[3] ?? "")?.[1];
const privateAssets = /\bPrivateAssets\s*=\s*"all"/i.test(attributes) || /<PrivateAssets>\s*all\s*<\/PrivateAssets>/i.test(match[3] ?? "");
return { name, constraint: version, isDev: privateAssets };
}).filter(
(entry) => entry !== null
).sort((left, right) => left.name.localeCompare(right.name))
};
},
parseLockfile(content) {
const parsed = readJson(content);
const seen = /* @__PURE__ */ new Map();
for (const framework of Object.values(parsed?.dependencies ?? {})) {
for (const [name, entry] of Object.entries(framework)) {
if (entry.type === "Direct" && typeof entry.resolved === "string" && !seen.has(name)) {
seen.set(name, entry.resolved);
}
}
}
return {
ecosystem: "dotnet",
packages: [...seen.entries()].map(([name, version]) => ({ name, version })).sort((left, right) => left.name.localeCompare(right.name))
};
}
};
// src/introspection/ecosystems/go.ts
init_esm_shims();
var goParser = {
ecosystem: "go",
packageManager: "go",
manifestFiles: ["go.mod"],
lockfileFiles: ["go.sum"],
parseManifest(content) {
return {
ecosystem: "go",
packages: goRequireLines(content).map((line) => /^([^\s]+)\s+([^\s]+)/.exec(line)).filter((match) => match !== null).map((match) => ({
name: match[1],
constraint: match[2],
isDev: false
}))
};
},
parseLockfile(content) {
return {
ecosystem: "go",
packages: content.split("\n").map((line) => line.trim()).filter((line) => line !== "").map((line) => {
const [name, version] = line.split(/\s+/, 3);
if (!name || !version) {
return null;
}
return { name, version };
}).filter((entry) => entry !== null)
};
}
};
function goRequireLines(content) {
const lines = [];
let inBlock = false;
for (const raw of content.split("\n")) {
const line = raw.replace(/\/\/.*$/, "").trim();
if (inBlock) {
if (line === ")") {
inBlock = false;
} else if (line !== "") {
lines.push(line);
}
continue;
}
if (/^require\s*\($/.test(line)) {
inBlock = true;
} else if (line.startsWith("require ")) {
lines.push(line.slice("require ".length).trim());
}
}
return lines;
}
function goLocalReplacements(content) {
const names = [];
let inBlock = false;
for (const raw of content.split("\n")) {
const line = raw.replace(/\/\/.*$/, "").trim();
if (/^replace\s*\($/.test(line)) {
inBlock = true;
continue;
}
if (inBlock && line === ")") {
inBlock = false;
continue;
}
const body = inBlock ? line : line.startsWith("replace ") ? line.slice(8).trim() : null;
const match = body === null ? null : /^([^\s]+)(?:\s+[^\s]+)?\s*=>\s*([^\s]+)/.exec(body);
if (match?.[1] && match[2] && /^(\.{1,2}\/|\/)/.test(match[2])) {
names.push(match[1]);
}
}
return names;
}
// src/introspection/ecosystems/jvm.ts
init_esm_shims();
var jvmParser = {
ecosystem: "jvm",
packageManager: "gradle",
manifestFiles: ["build.gradle", "build.gradle.kts", "pom.xml"],
lockfileFiles: ["gradle.lockfile"],
parseManifest(content, filename) {
if (filename === "pom.xml") {
return {
ecosystem: "jvm",
packages: Array.from(
content.matchAll(
/<dependency>[\s\S]*?<groupId>([^<]+)<\/groupId>[\s\S]*?<artifactId>([^<]+)<\/artifactId>[\s\S]*?(?:<version>([^<]+)<\/version>)?[\s\S]*?<\/dependency>/g
)
).map((match) => ({
name: `${match[1]}:${match[2]}`,
constraint: match[3],
isDev: false
}))
};
}
return {
ecosystem: "jvm",
packages: Array.from(
content.matchAll(
/(?:implementation|api|compileOnly|runtimeOnly|testImplementation)\s*\(?["']([^:"']+):([^:"']+):([^"')]+)["']\)?/g
)
).map((match) => ({
name: `${match[1]}:${match[2]}`,
constraint: match[3],
isDev: match[0].startsWith("test")
}))
};
},
parseLockfile(content) {
return {
ecosystem: "jvm",
packages: content.split("\n").map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("#")).map((line) => {
const [name, version] = line.split("=");
if (!name || !version) {
return null;
}
return { name, version };
}).filter((entry) => entry !== null)
};
}
};
// src/introspection/ecosystems/node-npm.ts
init_esm_shims();
var npmParser = {
ecosystem: "node",
packageManager: "npm",
manifestFiles: ["package.json"],
lockfileFiles: ["package-lock.json", "yarn.lock"],
parseManifest(content) {
const parsed = readJson(content);
return {
ecosystem: "node",
packages: [
...Object.entries(parsed?.dependencies ?? {}).map(([name, constraint]) => ({
name,
constraint,
isDev: false
})),
...Object.entries(parsed?.devDependencies ?? {}).map(([name, constraint]) => ({
name,
constraint,
isDev: true
}))
].sort((left, right) => left.name.localeCompare(right.name)),
scripts: parsed?.scripts ?? {}
};
},
parseLockfile(content, filename) {
if (filename === "yarn.lock") {
return { ecosystem: "node", packages: parseYarnLock(content) };
}
const parsed = readJson(content);
return {
ecosystem: "node",
packages: Object.entries(parsed?.packages ?? {}).filter(([key, value]) => key.startsWith("node_modules/") && value.version !== void 0).map(([key, value]) => ({
name: key.replace(/^node_modules\//, ""),
version: value.version
})).sort((left, right) => left.name.localeCompare(right.name))
};
}
};
function parseYarnLock(content) {
const entries = /* @__PURE__ */ new Map();
for (const block of content.split(/\n(?=\S)/)) {
const header = block.split("\n")[0]?.trim() ?? "";
if (header === "" || header.startsWith("#") || header.startsWith("__metadata")) {
continue;
}
const first = header.replace(/:$/, "").split(",")[0]?.trim().replace(/^"|"$/g, "") ?? "";
const at = first.indexOf("@", first.startsWith("@") ? 1 : 0);
const name = at > 0 ? first.slice(0, at) : first;
const version = /^\s+version:?\s+"?([^"\s]+)"?/m.exec(block)?.[1];
const resolved = /^\s+(?:resolved|resolution):?\s+"?([^"\s]+)"?/m.exec(block)?.[1];
if (name !== "" && version !== void 0 && !entries.has(name)) {
entries.set(name, resolved === void 0 ? { name, version } : { name, version, resolved });
}
}
return [...entries.values()].sort((left, right) => left.name.localeCompare(right.name));
}
// src/introspection/ecosystems/node-pnpm.ts
init_esm_shims();
var import_yaml2 = __toESM(require_dist(), 1);
var pnpmParser = {
ecosystem: "node",
packageManager: "pnpm",
manifestFiles: ["package.json"],
lockfileFiles: ["pnpm-lock.yaml"],
parseManifest(content) {
const parsed = readJson(content);
return {
ecosystem: "node",
packages: [
...Object.entries(parsed?.dependencies ?? {}).map(([name, constraint]) => ({
name,
constraint,
isDev: false
})),
...Object.entries(parsed?.devDependencies ?? {}).map(([name, constraint]) => ({
name,
constraint,
isDev: true
}))
].sort((left, right) => left.name.localeCompare(right.name)),
scripts: parsed?.scripts ?? {}
};
},
parseLockfile(content) {
let parsed;
try {
parsed = import_yaml2.default.parse(content);
} catch {
parsed = null;
}
return {
ecosystem: "node",
packages: Object.entries(parsed?.packages ?? {}).map(([key, value]) => {
const match = key.match(/^\/?(@?[^@/]+(?:\/[^@/]+)?)@([^(@]+)?/);
const version = value.version ?? match?.[2];
if (!match?.[1] || version === void 0) {
return null;
}
return {
name: match[1],
version
};
}).filter((entry) => entry !== null).sort((left, right) => left.name.localeCompare(right.name))
};
}
};
// src/introspection/ecosystems/php.ts
init_esm_shims();
var phpParser = {
ecosystem: "php",
packageManager: "composer",
manifestFiles: ["composer.json"],
lockfileFiles: ["composer.lock"],
parseManifest(content) {
const parsed = readJson(content);
return {
ecosystem: "php",
packages: [
...Object.entries(parsed?.require ?? {}).map(([name, constraint]) => ({
name,
constraint,
isDev: false
})),
...Object.entries(parsed?.["require-dev"] ?? {}).map(([name, constraint]) => ({
name,
constraint,
isDev: true
}))
].sort((left, right) => left.name.localeCompare(right.name))
};
},
parseLockfile(content) {
const parsed = readJson(content);
return {
ecosystem: "php",
packages: [...parsed?.packages ?? [], ...parsed?.["packages-dev"] ?? []].map((pkg) => ({ name: pkg.name, version: pkg.version })).sort((left, right) => left.name.localeCompare(right.name))
};
}
};
// src/introspection/ecosystems/python.ts
init_esm_shims();
function parsePythonManifest(content, filename) {
const trimmed = filename.toLowerCase();
if (trimmed === "requirements.txt") {
return {
ecosystem: "python",
packages: parseKeyValueLines(content, "==").map(({ name, value }) => ({
name,
constraint: value,
isDev: false
}))
};
}
if (trimmed === "pyproject.toml") {
return { ecosystem: "python", packages: parsePyproject(content) };
}
const matches = Array.from(
content.matchAll(/^[ \t]*["']?([A-Za-z0-9_.-]+)["']?\s*(?:[<>=!~]=?.+)?/gm)
);
return {
ecosystem: "python",
packages: matches.map((match) => match[1]?.trim()).filter((name) => Boolean(name) && !name.startsWith("[")).map((name) => ({ name, isDev: false }))
};
}
var pythonParser = {
ecosystem: "python",
packageManager: "pip",
manifestFiles: ["requirements.txt", "pyproject.toml", "Pipfile", "setup.py"],
lockfileFiles: ["Pipfile.lock", "poetry.lock", "uv.lock"],
parseManifest(content, filename) {
return parsePythonManifest(content, filename);
},
parseLockfile(content, filename) {
if (filename === "Pipfile.lock") {
try {
const parsed = JSON.parse(content);
return {
ecosystem: "python",
packages: [
...Object.entries(parsed.default ?? {}),
...Object.entries(parsed.develop ?? {})
].filter(([, value]) => typeof value.version === "string").map(([name, value]) => ({ name, version: value.version }))
};
} catch {
return { ecosystem: "python", packages: [] };
}
}
const tomlPackages = Array.from(
content.matchAll(/\[\[package\]\]\s*\nname\s*=\s*"([^"]+)"\s*\nversion\s*=\s*"([^"]+)"/g)
).map((match) => ({ name: match[1], version: match[2] }));
if (tomlPackages.length > 0) {
return { ecosystem: "python", packages: tomlPackages };
}
return {
ecosystem: "python",
packages: parseKeyValueLines(content, "==").map(({ name, value }) => ({
name,
version: value
}))
};
}
};
function parsePyproject(content) {
const packages = /* @__PURE__ */ new Map();
const add = (name, constraint, isDev) => {
if (name === "" || name.toLowerCase() === "python" || packages.has(name)) {
return;
}
packages.set(name, constraint === void 0 ? { name, isDev } : { name, constraint, isDev });
};
const requirement = (spec, isDev) => {
const match = /^\s*([A-Za-z0-9_.-]+)(?:\[[^\]]*\])?\s*(.*)$/.exec(spec);
if (match?.[1]) {
add(match[1], match[2]?.trim() || void 0, isDev);
}
};
let section = "";
let arrayFor = null;
for (const raw of content.split("\n")) {
const line = raw.trim();
if (arrayFor !== null) {
for (const quoted of line.matchAll(/["']([^"']+)["']/g)) {
requirement(quoted[1] ?? "", arrayFor === "dev");
}
if (line.includes("]")) {
arrayFor = null;
}
continue;
}
const header = /^\[([^\]]+)\]$/.exec(line);
if (header) {
section = header[1]?.trim() ?? "";
continue;
}
const isDevTable = /^tool\.poetry\.(dev-dependencies|group\.[^.]+\.dependencies)$/.test(
section
);
if (section === "tool.poetry.dependencies" || isDevTable) {
const entry = /^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/.exec(line);
if (entry?.[1]) {
const plain = /^"([^"]*)"$/.exec(entry[2]?.trim() ?? "");
const table = /\bversion\s*=\s*"([^"]+)"/.exec(entry[2] ?? "");
const path = /\bpath\s*=\s*"([^"]+)"/.exec(entry[2] ?? "");
const git = /\bgit\s*=\s*"([^"]+)"/.exec(entry[2] ?? "");
add(
entry[1],
plain?.[1] ?? table?.[1] ?? (path ? `path:${path[1]}` : git ? `git:${git[1]}` : void 0),
isDevTable
);
}
continue;
}
const isProjectDeps = section === "project" && /^dependencies\s*=\s*\[/.test(line);
const isDevGroup = (section === "project.optional-dependencies" || section === "dependency-groups") && /^(dev|test|tests|lint)\s*=\s*\[/.test(line);
if (isProjectDeps || isDevGroup) {
const body = line.slice(line.indexOf("[") + 1);
for (const quoted of body.matchAll(/["']([^"']+)["']/g)) {
requirement(quoted[1] ?? "", isDevGroup);
}
if (!body.includes("]")) {
arrayFor = isDevGroup ? "dev" : "deps";
}
}
}
return [...packages.values()].sort((left, right) => left.name.localeCompare(right.name));
}
// src/introspection/ecosystems/ruby.ts
init_esm_shims();
var rubyParser = {
ecosystem: "ruby",
packageManager: "bundler",
manifestFiles: ["Gemfile"],
lockfileFiles: ["Gemfile.lock"],
parseManifest(content) {
return {
ecosystem: "ruby",
packages: Array.from(
content.matchAll(/gem\s+["']([^"']+)["'](?:,\s*["']([^"']+)["'])?/g)
).map((match) => ({
name: match[1],
constraint: match[2],
isDev: false
}))
};
},
parseLockfile(content) {
return {
ecosystem: "ruby",
packages: parseKeyValueLines(
content.split("\n").map((line) => line.trim()).filter((line) => line.startsWith(" ") === false && /\(.+\)/.test(line)).join("\n"),
" ("
).map(({ name, value }) => ({
name,
version: value.replace(/\)$/, "")
}))
};
}
};
// src/introspection/ecosystems/rust.ts
init_esm_shims();
var rustParser = {
ecosystem: "rust",
packageManager: "cargo",
manifestFiles: ["Cargo.toml"],
lockfileFiles: ["Cargo.lock"],
parseManifest(content) {
return {
ecosystem: "rust",
packages: parseCargoDependencies(content)
};
},
parseLockfile(content) {
return {
ecosystem: "rust",
packages: Array.from(
content.matchAll(
/\[\[package\]\][\s\S]*?name\s*=\s*"([^"]+)"[\s\S]*?version\s*=\s*"([^"]+)"/g
)
).map((match) => ({
name: match[1],
version: match[2]
}))
};
}
};
function parseCargoDependencies(content) {
const packages = [];
let section = null;
for (const raw of content.split("\n")) {
const line = raw.trim();
const header = /^\[([^\]]+)\]$/.exec(line);
if (header) {
const name = header[1]?.trim() ?? "";
section = /(^|\.)dependencies$/.test(name) ? name.endsWith("dev-dependencies") ? "dev" : name.endsWith("build-dependencies") ? null : "deps" : null;
continue;
}
if (section === null) {
continue;
}
const entry = /^([A-Za-z0-9_-]+)\s*=\s*(.+)$/.exec(line);
if (!entry?.[1] || !entry[2]) {
continue;
}
packages.push({
name: entry[1],
constraint: cargoConstraint(entry[2]),
isDev: section === "dev"
});
}
return packages.sort((left, right) => left.name.localeCompare(right.name));
}
function cargoConstraint(value) {
const plain = /^"([^"]*)"$/.exec(value.trim());
if (plain) {
return plain[1] ?? "unknown";
}
const path = /\bpath\s*=\s*"([^"]+)"/.exec(value)?.[1];
if (path !== void 0) {
return `path:${path}`;
}
const git = /\bgit\s*=\s*"([^"]+)"/.exec(value)?.[1];
if (git !== void 0) {
return `git:${git}`;
}
return /\bversion\s*=\s*"([^"]+)"/.exec(value)?.[1] ?? "unknown";
}
// src/introspection/ecosystems/registry.ts
var EcosystemParserRegistry = class {
parsers = [];
constructor(parsers = []) {
for (const parser of parsers) {
this.register(parser);
}
}
register(parser) {
this.parsers.push(parser);
}
list() {
return [...this.parsers];
}
getKnownFiles() {
return Array.from(
new Set(this.parsers.flatMap((parser) => [...parser.manifestFiles, ...parser.lockfileFiles]))
).sort();
}
async parseProject(projectRoot) {
const results = [];
for (const parser of this.parsers) {
const manifest = await findFirstExistingFile(projectRoot, parser.manifestFiles);
if (manifest === null) {
continue;
}
const parsedManifest = parser.parseManifest(manifest.content, manifest.filename);
const lockfile = await findFirstExistingFile(projectRoot, parser.lockfileFiles);
const parsedLockfile = lockfile === null ? { ecosystem: parser.ecosystem, packages: [] } : parser.parseLockfile(lockfile.content, lockfile.filename);
const lockedVersions = new Map(parsedLockfile.packages.map((pkg) => [pkg.name, pkg.version]));
results.push({
toolchain: {
ecosystem: parser.ecosystem,
package_manager: parser.packageManager,
lockfile: lockfile?.filename ?? parser.lockfileFiles[0] ?? ""
},
packages: parsedManifest.packages.map((pkg) => ({
name: pkg.name,
version_constraint: pkg.constraint ?? "unknown",
locked_version: lockedVersions.get(pkg.name) ?? pkg.constraint ?? "unknown",
ecosystem: parser.ecosystem,
is_dev: pkg.isDev ?? false,
manifest: manifest.filename
})).sort((left, right) => left.name.localeCompare(right.name))
});
}
return results;
}
};
async function findFirstExistingFile(projectRoot, filenames) {
for (const filename of expandManifestNames(projectRoot, filenames)) {
const content = await readProjectFile(projectRoot, filename);
if (content !== null) {
return { filename, content };
}
}
return null;
}
function expandManifestNames(projectRoot, filenames) {
return filenames.flatMap(
(filename) => filename.includes("*") ? import_fast_glob.default.sync(filename, { cwd: projectRoot, onlyFiles: true, deep: 1 }).sort() : [filename]
);
}
function createDefaultEcosystemParserRegistry() {
return new EcosystemParserRegistry([
pnpmParser,
npmParser,
phpParser,
dartParser,
pythonParser,
rubyParser,
jvmParser,
goParser,
rustParser,
dotnetParser
]);
}
// src/rules-discovery/baseline.ts
init_esm_shims();
import { join as join3 } from "path";
// src/rules-discovery/files.ts
init_esm_shims();
import { mkdirSync, writeFileSync } from "fs";
import { dirname } from "path";
function atomicWriteText(absPath, text) {
mkdirSync(dirname(absPath), { recursive: true });
const tmp = `${absPath}.${process.pid}.tmp`;
writeFileSync(tmp, text, "utf8");
renameWithRetrySync(tmp, absPath);
}
function atomicWriteJson(absPath, value) {
atomicWriteText(absPath, `${JSON.stringify(value, null, 2)}
`);
}
function readJsonFile(absPath) {
const text = readTextIfExistsSync(absPath);
if (text === null) {
return { state: "missing" };
}
try {
return { state: "ok", value: JSON.parse(text) };
} catch (error) {
return { state: "unreadable", error: error instanceof Error ? error.message : String(error) };
}
}
// src/rules-discovery/paths.ts
init_esm_shims();
import { join as join2 } from "path";
var RULES_DISCOVERY_DIR = ".paqad/ledger/rules-discovery";
var RULES_DISCOVERY_BASELINE = `${RULES_DISCOVERY_DIR}/baseline.json`;
var RULES_DISCOVERY_NUDGE_STATE = ".paqad/rules-discovery-nudge.json";
var STACK_SNAPSHOT_PATH = ".paqad/stack-snapshot.json";
var RULES_ROOT = "docs/instructions/rules";
var PROJECT_RULES_DIR = `${RULES_ROOT}/project`;
var RUN_FILES = {
run: "run.json",
inventory: "stack-inventory.json",
fromDecisions: "from-decisions.json",
fromResearch: "from-research.json",
research: "research.json",
sources: "sources.json",
judgements: "proposal-judgements.json",
proposal: "proposal.json",
proposalMarkdown: "proposal.md",
apply: "apply.json",
report: "report.json"
};
var RUN_ID_RE = /^rd-\d{8}-\d{6}(?:-[a-z0-9]{1,8})?$/;
function isRunId(value) {
return RUN_ID_RE.test(value);
}
function mintRunId(now, suffix) {
const stamp = now.toISOString().replace(/[-:]/g, "").replace("T", "-").slice(0, 15);
return suffix === void 0 ? `rd-${stamp}` : `rd-${stamp}-${suffix}`;
}
function runDirPath(runId) {
if (!isRunId(runId)) {
throw new Error(`invalid rules discovery run id "${runId}"`);
}
return `${RULES_DISCOVERY_DIR}/${runId}`;
}
function runFilePath(runId, file) {
return `${runDirPath(runId)}/${RUN_FILES[file]}`;
}
function absRunFile(projectRoot, runId, file) {
return join2(projectRoot, runFilePath(runId, file));
}
// src/rules-discovery/baseline.ts
function readBaseline(projectRoot) {
const read = readJsonFile(join3(projectRoot, RULES_DISCOVERY_BASELINE));
if (read.state !== "ok" || !Array.isArray(read.value.packages)) {
return null;
}
return read.value;
}
function baselineLabel(baseline) {
return baseline.inventory ?? baseline.seeded_from ?? RULES_DISCOVERY_BASELINE;
}
function seedBaselineFromSnapshot(projectRoot, now = /* @__PURE__ */ new Date()) {
const snapshot = readJsonFile(join3(projectRoot, STACK_SNAPSHOT_PATH));
if (snapshot.state !== "ok" || !Array.isArray(snapshot.value.packages)) {
return null;
}
const baseline = {
schema_version: 1,
source: "seed",
inventory: null,
run_id: null,
seeded_from: STACK_SNAPSHOT_PATH,
updated_at: now.toISOString(),
packages: snapshot.value.packages.map((pkg) => ({
name: pkg.name,
ecosystem: pkg.ecosystem,
declared: knownVersion(pkg.version_constraint),
installed: knownVersion(pkg.locked_version),
dev: pkg.is_dev
}))
};
atomicWriteJson(join3(projectRoot, RULES_DISCOVERY_BASELINE), baseline);
return baseline;
}
function readOrSeedBaseline(projectRoot, now = /* @__PURE__ */ new Date()) {
return readBaseline(projectRoot) ?? seedBaselineFromSnapshot(projectRoot, now);
}
function moveBaselineToRun(projectRoot, inventory, now = /* @__PURE__ */ new Date()) {
const baseline = {
schema_version: 1,
source: "run",
inventory: runFilePath(inventory.run_id, "inventory"),
run_id: inventory.run_id,
seeded_from: null,
updated_at: now.toISOString(),
packages: inventory.packages.filter((pkg) => pkg.status !== "removed").map((pkg) => ({
name: pkg.name,
ecosystem: pkg.ecosystem,
declared: pkg.declared,
installed: pkg.installed,
dev: pkg.dev
}))
};
atomicWriteJson(join3(projectRoot, RULES_DISCOVERY_BASELINE), baseline);
return baseline;
}
function knownVersion(value) {
return value === void 0 || value === null || value === "" || value === "unknown" ? null : value;
}
// src/rules-discovery/inventory.ts
init_esm_shims();
var import_yaml3 = __toESM(require_dist(), 1);
import { readFileSync as readFileSync2 } from "fs";
import { basename as basename2, dirname as dirname2, join as join7, posix } from "path";
// src/introspection/stack-introspector.ts
init_esm_shims();
import { join as join6 } from "path";
// src/repository/discovery.ts
init_esm_shims();
import { lstat, readdir } from "fs/promises";
import { basename, join as join4 } from "path";
var DEFAULT_SCAN_MAX_DEPTH = 5;
var PROJECT_MARKERS = /* @__PURE__ */ new Map([
["package.json", "node"],
["composer.json", "php"],
["pubspec.yaml", "dart"],
["pyproject.toml", "python"],
["requirements.txt", "python"],
["Pipfile", "python"],
["setup.py", "python"],
["Gemfile", "ruby"],
["go.mod", "go"],
["Cargo.toml", "rust"],
["pom.xml", "jvm"],
["build.gradle", "jvm"],
["build.gradle.kts", "jvm"],
["artisan", "php"],
["manage.py", "python"]
]);
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
".git",
".next",
".nuxt",
".dart_tool",
".gradle",
".turbo",
"build",
"coverage",
"dist",
"docs",
"modules",
"node_modules",
"out",
"target",
"tests",
"vendor"
]);
var NON_CANONICAL_DIRECTORIES = /* @__PURE__ */ new Set([
"__fixtures__",
"demo",
"demos",
"example",
"examples",
"fixtures",
"test-fixtures",
"tmp"
]);
var COMPONENT_DIRECTORY_NAMES = /* @__PURE__ */ new Set(["client", "frontend", "ui", "web"]);
async function discoverRepositoryContext(projectRoot, options) {
const maxDepth = options?.maxDepth ?? DEFAULT_SCAN_MAX_DEPTH;
const ignoredPaths = /* @__PURE__ */ new Set();
const candidates = /* @__PURE__ */ new Map();
const tree = await buildTree(projectRoot, ".", 0, maxDepth);
const checkPaths = collectCheckPaths(tree);
const visible = new Set(checkPaths.length > 0 ? dropGitIgnored(projectRoot, checkPaths) : []);
resolveTree(tree, visible, candidates, ignoredPaths);
const sortedCandidates = Array.from(candidates.values()).sort(compareCandidates);
const projects = classifyProjects(sortedCandidates);
const applications = buildApplications(projects);
const primaryProjectRoot = resolvePrimaryProjectRoot(projects, applications);
return {
selected_root: projectRoot,
scan_max_depth: maxDepth,
ignored_paths: Array.from(ignoredPaths).sort(),
projects,
applications,
primary_project_root: primaryProjectRoot
};
}
function prefixRepositoryPath(root, relativePath) {
if (root === "." || root === "") {
return relativePath;
}
return join4(root, relativePath);
}
function emptyNode(relativeDir) {
return { relativeDir, markerCandidates: [], markerPaths: [], staticIgnored: [], children: [] };
}
async function buildTree(projectRoot, relativeDir, depth, maxDepth) {
const absoluteDir = relativeDir === "." ? projectRoot : join4(projectRoot, relativeDir);
const node = emptyNode(relativeDir);
let entries;
try {
entries = await readdir(absoluteDir, { withFileTypes: true });
} catch {
return node;
}
const entryNames = new Set(entries.map((entry) => entry.name));
node.markerCandidates = Array.from(PROJECT_MARKERS.keys()).filter(
(marker) => entryNames.has(marker)
);
node.markerPaths = node.markerCandidates.map((marker) => repositoryPath(relativeDir, marker));
if (depth >= maxDepth) {
return node;
}
for (const entry of entries) {
const childRelativePath = repositoryPath(relativeDir, entry.name);
if (entry.name.startsWith(".")) {
node.staticIgnored.push(childRelativePath);
continue;
}
if (!entry.isDirectory()) {
continue;
}
if (IGNORED_DIRECTORIES.has(entry.name) || NON_CANONICAL_DIRECTORIES.has(entry.name)) {
node.staticIgnored.push(childRelativePath);
continue;
}
const vcsBoundary = await hasVcsBoundary(join4(absoluteDir, entry.name));
const childNode = vcsBoundary ? emptyNode(childRelativePath) : await buildTree(projectRoot, childRelativePath, depth + 1, maxDepth);
node.children.push({ relativePath: childRelativePath, vcsBoundary, node: childNode });
}
return node;
}
function collectCheckPaths(node, out = []) {
for (const markerPath of node.markerPaths) {
out.push(markerPath);
}
for (const child of node.children) {
out.push(child.relativePath);
collectCheckPaths(child.node, out);
}
return out;
}
function resolveTree(node, visible, candidates, ignoredPaths) {
const markers = node.markerCandidates.filter((_marker, index) => {
const markerPath = node.markerPaths[index];
if (visible.has(markerPath)) {
return true;
}
ignoredPaths.add(markerPath);
return false;
});
if (markers.length > 0) {
const ecosystems = Array.from(
new Set(markers.map((marker) => PROJECT_MARKERS.get(marker)).filter(isDefined))
).sort();
candidates.set(node.relativeDir, {
root: node.relativeDir,
role: "standalone",
parent_root: null,
markers: markers.sort(),
ecosystems
});
}
for (const staticIgnoredPath of node.staticIgnored) {
ignoredPaths.add(staticIgnoredPath);
}
for (const child of node.children) {
if (!visible.has(child.relativePath) || child.vcsBoundary) {
ignoredPaths.add(child.relativePath);
continue;
}
resolveTree(child.node, visible, candidates, ignoredPaths);
}
}
function repositoryPath(relativeDir, entryName) {
return toPosixPath(relativeDir === "." ? entryName : join4(relativeDir, entryName));
}
async function hasVcsBoundary(absoluteDir) {
try {
await lstat(join4(absoluteDir, ".git"));
return true;
} catch {
return false;
}
}
function classifyProjects(candidates) {
const byRoot = new Map(candidates.map((candidate) => [candidate.root, candidate]));
const result = [];
for (const candidate of candidates) {
const ancestors = findAncestorCandidates(candidate.root, byRoot);
const parent = ancestors[0] ?? null;
const role = parent && isLinkedComponent(parent, candidate) ? "component" : "standalone";
result.push({
...candidate,
role,
parent_root: role === "component" ? parent.root : null
});
}
return result.sort(compareCandidates);
}
function findAncestorCandidates(root, candidates) {
if (root === ".") {
return [];
}
const segments = root.split("/").filter(Boolean);
const ancestors = [];
for (let index = segments.length - 1; index >= 1; index -= 1) {
const candidate = candidates.get(segments.slice(0, index).join("/"));
if (candidate) {
ancestors.push(candidate);
}
}
const selectedRoot = candidates.get(".");
if (selectedRoot) {
ancestors.push(selectedRoot);
}
return ancestors.sort(compareCandidates);
}
function isLinkedComponent(parent, candidate) {
const candidateName = basename(candidate.root);
const candidateNodeOnly = candidate.ecosystems.length > 0 && candidate.ecosystems.every((ecosystem) => ecosystem === "node");
const parentHasNonNode = parent.ecosystems.some((ecosystem) => ecosystem !== "node");
return candidateNodeOnly && parentHasNonNode && COMPONENT_DIRECTORY_NAMES.has(candidateName);
}
function buildApplications(projects) {
const standaloneRoots = projects.filter((project) => project.role === "standalone").map((project) => project.root);
return standaloneRoots.map((root) => ({
root,
component_roots: projects.filter((project) => project.parent_root === root).map((project) => project.root).sort()
}));
}
function resolvePrimaryProjectRoot(projects, applications) {
if (applications.length === 0) {
return null;
}
const byRoot = new Map(projects.map((project) => [project.root, project]));
return [...applications].sort((left, right) => {
const leftProject = byRoot.get(left.root);
const rightProject = byRoot.get(right.root);
return scoreCandidate(rightProject) - scoreCandidate(leftProject) || compareByRoot(left.root, right.root);
})[0]?.root ?? null;
}
function compareCandidates(left, right) {
return compareByRoot(left.root, right.root);
}
function compareByRoot(left, right) {
return depthOf(left) - depthOf(right) || left.localeCompare(right);
}
function depthOf(root) {
return root === "." ? 0 : root.split("/").filter(Boolean).length;
}
function scoreCandidate(candidate) {
if (!candidate) {
return -1;
}
return candidate.markers.reduce((score, marker) => score + markerScore(marker), 0) - depthOf(candidate.root);
}
function markerScore(marker) {
switch (marker) {
case "composer.json":
case "package.json":
case "pubspec.yaml":
return 5;
case "artisan":
case "manage.py":
return 4;
case "pyproject.toml":
case "Gemfile":
case "go.mod":
case "Cargo.toml":
case "pom.xml":
case "build.gradle":
case "build.gradle.kts":
return 3;
default:
return 1;
}
}
function isDefined(value) {
return value !== void 0;
}
// src/introspection/environment-traits.ts
init_esm_shims();
import { existsSync, readFileSync } from "fs";
import { join as join5 } from "path";
var COMPOSE_FILES = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
function detectEnvironmentTraits(projectRoot, options) {
const packageNames = new Set(options?.packageNames ?? []);
const traits = /* @__PURE__ */ new Set();
const sources = [];
const signals = [];
for (const file of COMPOSE_FILES) {
if (existsSync(join5(projectRoot, file))) {
traits.add("compose");
sources.push({
file,
kind: "config",
detail: "Detected docker compose environment from compose configuration"
});
signals.push({
signal: "compose file exists",
file,
implies: "compose",
confidence: "high"
});
break;
}
}
const dockerFiles = listDockerIndicators(projectRoot);
if (dockerFiles.length > 0) {
traits.add("docker");
for (const file of dockerFiles) {
sources.push({
file,
kind: "config",
detail: "Detected docker environment from Docker build assets"
});
signals.push({
signal: "docker build asset exists",
file,
implies: "docker",
confidence: "medium"
});
}
}
if (packageNames.has("laravel/sail")) {
traits.add("sail");
sources.push({
file: "composer.json",
kind: "manifest",
detail: "Detected laravel sail from composer dependency"
});
signals.push({
signal: "composer dependency laravel/sail",
file: "composer.json",
implies: "sail",
confidence: "high"
});
} else if (referencesSailBinary(projectRoot)) {
traits.add("sail");
sources.push({
file: detectSailReferenceFile(projectRoot) ?? "project files",
kind: "heuristic",
detail: "Detected laravel sail from vendor/bin/sail reference"
});
signals.push({
signal: "vendor/bin/sail reference found",
file: detectSailReferenceFile(projectRoot) ?? "project files",
implies: "sail",
confidence: "medium"
});
}
return {
traits: Array.from(traits).sort(),
sources,
signals
};
}
function listDockerIndicators(projectRoot) {
const indicators = [];
const directFiles = ["Dockerfile", "Dockerfile.dev", "Dockerfile.prod"];
for (const file of directFiles) {
if (existsSync(join5(projectRoot, file))) {
indicators.push(file);
}
}
if (existsSync(join5(projectRoot, "docker"))) {
indicators.push("docker/");
}
if (existsSync(join5(projectRoot, ".docker"))) {
indicators.push(".docker/");
}
return indicators;
}
function referencesSailBinary(projectRoot) {
return detectSailReferenceFile(projectRoot) !== null;
}
function detectSailReferenceFile(projectRoot) {
const candidates = ["composer.json", "package.json"];
for (const relativePath of candidates) {
try {
const content = readFileSync(join5(projectRoot, relativePath), "utf8");
if (content.includes("vendor/bin/sail")) {
return relativePath;
}
} catch {
continue;
}
}
return null;
}
// src/introspection/stack-introspector.ts
var StackIntrospector = class {
cache = new StackSnapshotCache();
parserRegistry = createDefaultEcosystemParserRegistry();
async snapshot(projectRoot, options = {}) {
const persist = options.persist ?? true;
const repository = await discoverRepositoryContext(projectRoot);
const candidateRoots = repository.projects.map((project) => project.root);
const currentHashes = await this.cache.hashFiles(
projectRoot,
buildRepositoryHashInputs(candidateRoots, this.parserRegistry.getKnownFiles())
);
const cached = await this.cache.read(projectRoot);
if (cached !== null && JSON.stringify(cached.source_hashes) === JSON.stringify(currentHashes)) {
return cached;
}
const toolchains = [];
const packages = [];
const environmentSources = [];
const environmentTraits = /* @__PURE__ */ new Set();
for (const project of repository.projects) {
const absoluteRoot = project.root === "." ? projectRoot : join6(projectRoot, project.root);
const results = await this.parserRegistry.parseProject(absoluteRoot);
for (const result of results) {
toolchains.push({
...result.toolchain,
lockfile: prefixRepositoryPath(project.root, result.toolchain.lockfile)
});
packages.push(
...result.packages.map((pkg) => ({
...pkg,
root: project.root
}))
);
}
const packageNames = packages.filter((pkg) => pkg.root === project.root).map((pkg) => pkg.name);
const environment = detectEnvironmentTraits(absoluteRoot, { packageNames });
for (const trait of environment.traits) {
environmentTraits.add(trait);
}
environmentSources.push(
...environment.sources.map((source) => ({
...source,
file: prefixRepositoryPath(project.root, source.file)
}))
);
}
const hashedSources = Object.keys(currentHashes).sort().map((file) => ({
file,
kind: classifySourceKind(file),
detail: classifySourceKind(file) === "config" ? "Used for environment trait detection" : "Used for stack detection and version resolution"
}));
const sources = dedupeSources([...hashedSources, ...environmentSources]);
const snapshot = {
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
source_hashes: currentHashes,
toolchains: dedupeToolchains(toolchains),
packages,
profile: buildDetectedStackProfile({
toolchains: dedupeToolchains(toolchains),
packages,
sources,
detectedTraits: Array.from(environmentTraits).sort()
}),
repository
};
if (persist) {
await this.cache.write(projectRoot, snapshot);
}
return snapshot;
}
};
function buildRepositoryHashInputs(candidateRoots, knownFiles) {
const inputs = /* @__PURE__ */ new Set();
const extraFiles = [
"docker-compose.yml",
"docker-compose.yaml",
"compose.yml",
"compose.yaml",
"Dockerfile",
"Dockerfile.dev",
"Dockerfile.prod",
"docker",
".docker"
];
for (const root of candidateRoots.length > 0 ? candidateRoots : ["."]) {
for (const file of [...knownFiles, ...extraFiles]) {
inputs.add(prefixRepositoryPath(root, file));
}
}
return Array.from(inputs).sort();
}
function dedupeToolchains(toolchains) {
const seen = /* @__PURE__ */ new Set();
return toolchains.filter((toolchain) => {
const key = `${toolchain.ecosystem}:${toolchain.package_manager}:${toolchain.lockfile}`;
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
function classifySourceKind(file) {
if (file.endsWith(".lock") || file.includes("lock")) {
return "lockfile";
}
if (file === "docker-compose.yml" || file === "docker-compose.yaml" || file === "compose.yml" || file === "compose.yaml" || file === "Dockerfile" || file === "Dockerfile.dev" || file === "Dockerfile.prod" || file === "docker" || file === ".docker") {
return "config";
}
return "manifest";
}
function dedupeSources(sources) {
const seen = /* @__PURE__ */ new Set();
return sources.filter((source) => {
const key = `${source.file}:${source.kind}:${source.detail}`;
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
// src/rules-discovery/inventory.ts
var PUBLIC_REGISTRY_HOSTS = {
node: ["registry.npmjs.org", "registry.yarnpkg.com", "registry.npmmirror.com"],
rust: ["github.com/rust-lang/crates.io-index", "index.crates.io"]
};
function knownManifestNames() {
return Array.from(
new Set(
createDefaultEcosystemParserRegistry().list().flatMap((parser) => parser.manifestFiles)
)
).sort();
}
function findManifests(projectRoot) {
const parsers = createDefaultEcosystemParserRegistry().list();
const names = knownManifestNames();
const files = scanWorkingTree(
projectRoot,
names.map((name) => `**/${name}`),
DEFAULT_IGNORE_GLOBS
);
return files.flatMap((manifest) => {
const candidates = parsers.filter(
(parser) => parser.manifestFiles.some((pattern) => matchesManifestName(basename2(manifest), pattern))
);
if (candidates.length === 0) {
return [];
}
for (const dir of ancestorDirs(dirname2(manifest))) {
for (const parser of candidates) {
const lock = parser.lockfileFiles.find(
(file) => readTextIfExistsSync(join7(projectRoot, dir, file)) !== null
);
if (lock !== void 0) {
return [{ manifest, parser, lockfile: dir === "." ? lock : `${dir}/${lock}` }];
}
}
}
return [{ manifest, parser: candidates[0], lockfile: null }];
});
}
function matchesManifestName(name, pattern) {
return pattern.startsWith("*") ? name.endsWith(pattern.slice(1)) : name === pattern;
}
function ancestorDirs(dir) {
const dirs = [];
let current = dir === "" ? "." : dir;
while (true) {
dirs.push(current);
if (current === ".") {
return dirs;
}
const parent = posix.dirname(current);
current = parent === current ? "." : parent;
}
}
function readPackages(projectRoot, pick) {
const content = readFileSync2(join7(projectRoot, pick.manifest), "utf8");
const manifest = pick.parser.parseManifest(content, basename2(pick.manifest));
const lockContent = pick.lockfile === null ? null : readFileSync2(join7(projectRoot, pick.lockfile), "utf8");
const locked = /* @__PURE__ */ new Map();
if (lockContent !== null && pick.lockfile !== null) {
for (const entry of pick.parser.parseLockfile(lockContent, basename2(pick.lockfile)).packages) {
locked.set(entry.name, entry.version);
}
}
const lockSources = lockContent === null || pick.lockfile === null ? /* @__PURE__ */ new Map() : lockfileSources(lockContent, basename2(pick.lockfile));
const replaced = pick.parser.ecosystem === "go" ? new Set(goLocalReplacements(content)) : /* @__PURE__ */ new Set();
return manifest.packages.map((pkg) => {
const declared = knownVersion(pkg.constraint);
const reason = sourceReason(declared) ?? (replaced.has(pkg.name) ? "local-path" : void 0) ?? registryReason(pick.parser.ecosystem, lockSources.get(pkg.name));
return {
name: pkg.name,
ecosystem: pick.parser.ecosystem,
declared,
installed: knownVersion(locked.get(pkg.name)),
manifest: pick.manifest,
dev: pkg.isDev ?? false,
...reason === void 0 ? {} : { privateReason: reason }
};
});
}
function sourceReason(declared) {
if (declared === null) {
return void 0;
}
if (declared.startsWith("workspace:")) {
return "workspace-source";
}
if (/^(file:|link:|path:|portal:|\.{1,2}\/|\/)/.test(declared)) {
return "local-path";
}
if (/^(git\+|git:|github:|gitlab:|bitbucket:|https?:\/\/|ssh:\/\/)/.test(declared)) {
return "git-source";
}
return void 0;
}
function registryReason(ecosystem, resolved) {
const publicHosts = PUBLIC_REGISTRY_HOSTS[ecosystem];
if (resolved === void 0 || publicHosts === void 0) {
return void 0;
}
if (/^(git\+|git:|github:)/.test(resolved)) {
return "git-source";
}
if (resolved.startsWith("path+") || resolved.startsWith("file:")) {
return "local-path";
}
if (!/^(https?:\/\/|registry\+|sparse\+)/.test(resolved)) {
return void 0;
}
return publicHosts.some((host) => resolved.includes(host)) ? void 0 : "private-registry";
}
function lockfileSources(content, filename) {
const sources = /* @__PURE__ */ new Map();
if (filename === "package-lock.json") {
try {
const parsed = JSON.parse(content);
for (const [key, value] of Object.entries(parsed.packages ?? {})) {
if (key.startsWith("node_modules/") && typeof value.resolved === "string") {
sources.set(key.slice("node_modules/".length), value.resolved);
}
}
} catch {
return sources;
}
} else if (filename === "yarn.lock") {
for (const entry of parseYarnLock(content)) {
if (entry.resolved !== void 0) {
sources.set(entry.name, entry.resolved);
}
}
} else if (filename === "pnpm-lock.yaml") {
try {
const parsed = import_yaml3.default.parse(content);
for (const [key, value] of Object.entries(parsed?.packages ?? {})) {
const name = /^\/?(@?[^@/]+(?:\/[^@/]+)?)@/.exec(key)?.[1];
const tarball = value?.resolution?.tarball;
if (name !== void 0 && typeof tarball === "string") {
sources.set(name, tarball);
}
}
} catch {
return sources;
}
} else if (filename === "Cargo.lock") {
for (const match of content.matchAll(
/\[\[package\]\]\s*\nname\s*=\s*"([^"]+)"[^[]*?\nsource\s*=\s*"([^"]+)"/g
)) {
sources.set(match[1], match[2]);
}
} else if (filename === "composer.lock") {
try {
const parsed = JSON.parse(content);
for (const entry of [...parsed.packages ?? [], ...parsed["packages-dev"] ?? []]) {
if (typeof entry.name === "string" && entry.dist?.type === "path") {
sources.set(entry.name, "path+local");
}
}
} catch {
return sources;
}
}
return sources;
}
function workspacePackageNames(projectRoot, picks) {
const names = /* @__PURE__ */ new Set();
for (const pick of picks) {
const text = readFileSync2(join7(projectRoot, pick.manifest), "utf8");
const file = basename2(pick.manifest);
if (file === "package.json" || file === "composer.json") {
try {
const name = JSON.parse(text).name;
if (typeof name === "string") names.add(name);
} catch {
continue;
}
} else if (file === "Cargo.toml") {
const name = /\[package\][^[]*?\nname\s*=\s*"([^"]+)"/.exec(text)?.[1];
if (name !== void 0) names.add(name);
} else if (file === "pubspec.yaml") {
const name = /^name:\s*([A-Za-z0-9_]+)/m.exec(text)?.[1];
if (name !== void 0) names.add(name);
} else if (file === "go.mod") {
const name = /^module\s+(\S+)/m.exec(text)?.[1];
if (name !== void 0) names.add(name);
}
}
return names;
}
function readPrivatePatterns(projectRoot, env = process.env) {
const raw = layeredConfigMap(projectRoot, env).get("rules_discovery_private_packages") ?? "";
return raw.split(",").map((entry) => entry.trim()).filter((entry) => entry !== "");
}
function matchesPrivatePattern(name, patterns) {
return patterns.some((pattern) => {
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
return new RegExp(`^${escaped}$`, "i").test(name);
});
}
var SOURCE_EXTENSIONS = {
node: "{ts,tsx,js,jsx,mjs,cjs,vue,svelte}",
python: "py",
go: "go",
rust: "rs",
ruby: "rb",
dart: "dart",
dotnet: "cs",
jvm: "{java,kt,kts,scala}"
};
function importSpecifiers(ecosystem, text) {
const all = (re) => Array.from(text.matchAll(re), (m) => m[1] ?? "");
switch (ecosystem) {
case "node":
return [
...all(/\bfrom\s+['"]([^'"]+)['"]/g),
...all(/\bimport\s+['"]([^'"]+)['"]/g),
...all(/\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g)
];
case "python":
return [
...all(/^\s*import\s+([A-Za-z0-9_.]+)/gm),
...all(/^\s*from\s+([A-Za-z0-9_.]+)\s+import\b/gm)
];
case "go":
return all(/^\s*(?:import\s+)?(?:[A-Za-z_.]+\s+)?"([^"\s]+)"\s*$/gm);
case "rust":
return all(/^\s*(?:pub\s+)?(?:use|extern\s+crate)\s+([A-Za-z0-9_]+)/gm);
case "ruby":
return all(/^\s*require\s+['"]([^'"]+)['"]/gm);
case "dart":
return all(/['"]package:([A-Za-z0-9_]+)\//g);
case "dotnet":
return all(/^\s*using\s+(?:static\s+)?([A-Za-z0-9_.]+)\s*;/gm);
case "jvm":
return all(/^\s*import\s+(?:static\s+)?([A-Za-z0-9_.]+)/gm);
default:
return [];
}
}
function importMatches(ecosystem, name, spec) {
switch (ecosystem) {
case "node":
return spec === name || spec.startsWith(`${name}/`);
case "python": {
const top = spec.split(".")[0]?.toLowerCase() ?? "";
return top === name.toLowerCase().replace(/-/g, "_");
}
case "go":
return spec === name || spec.startsWith(`${name}/`);
case "rust":
return spec === name.replace(/-/g, "_");
case "ruby":
return spec === name || spec.startsWith(`${name}/`);
case "dart":
return spec === name;
case "dotnet":
return spec === name || spec.startsWith(`${name}.`);
case "jvm": {
const group = name.split(":")[0] ?? "";
return group !== "" && (spec === group || spec.startsWith(`${group}.`));
}
default:
return false;
}
}
function countImports(projectRoot, packages) {
const counts = /* @__PURE__ */ new Map();
const ecosystems = new Set(packages.map((pkg) => pkg.ecosystem));
for (const ecosystem of ecosystems) {
const extension = SOURCE_EXTENSIONS[ecosystem];
const own = packages.filter((pkg) => pkg.ecosystem === ecosystem);
if (extension === void 0) {
for (const pkg of own) counts.set(packageKey(pkg), null);
continue;
}
for (const pkg of own) counts.set(packageKey(pkg), 0);
const files = scanWorkingTree(projectRoot, [`**/*.${extension}`], DEFAULT_IGNORE_GLOBS);
for (const file of files) {
const text = readTextIfExistsSync(join7(projectRoot, file));
if (text === null) continue;
const specs = importSpecifiers(ecosystem, text);
if (specs.length === 0) continue;
for (const pkg of own) {
if (specs.some((spec) => importMatches(ecosystem, pkg.name, spec))) {
const key = packageKey(pkg);
counts.set(key, (counts.get(key) ?? 0) + 1);
}
}
}
}
return counts;
}
function packageKey(pkg) {
return `${pkg.ecosystem}:${pkg.name}`;
}
function statusAgainst(pkg, baseline) {
if (baseline === null) {
return "unchanged";
}
const before = baseline.get(packageKey(pkg));
if (before === void 0) {
return "new";
}
const changed = pkg.installed !== null && before.installed !== null && pkg.installed !== before.installed || pkg.declared !== null && before.declared !== null && pkg.declared !== before.declared;
return changed ? "version-changed" : "unchanged";
}
async function detectFrameworks(projectRoot) {
const snapshot = await new StackIntrospector().snapshot(projectRoot, { persist: false });
return [...snapshot.profile.frameworks].sort();
}
async function buildStackInventory(input) {
const now = input.now ?? /* @__PURE__ */ new Date();
const picks = findManifests(input.projectRoot);
const raw = picks.flatMap((pick) => readPackages(input.projectRoot, pick));
const workspaceNames = workspacePackageNames(input.projectRoot, picks);
const patterns = readPrivatePatterns(input.projectRoot, input.env);
const baseline = readOrSeedBaseline(input.projectRoot, now);
const baselineMap = baseline === null ? null : baselineIndex(baseline);
const counts = countImports(input.projectRoot, raw);
const current = raw.map((pkg) => {
const reason = pkg.privateReason ?? (workspaceNames.has(pkg.name) ? "workspace-package" : void 0) ?? (matchesPrivatePattern(pkg.name, patterns) ? "configured" : void 0);
const importCount = counts.get(packageKey(pkg)) ?? null;
return {
name: pkg.name,
ecosystem: pkg.ecosystem,
declared: pkg.declared,
installed: pkg.installed,
manifest: pkg.manifest,
dev: pkg.dev,
import_count: importCount,
unused: importCount === 0,
status: statusAgainst(pkg, baselineMap),
private: reason !== void 0,
...reason === void 0 ? {} : { private_reason: reason }
};
});
const present = new Set(current.map(packageKey));
const removed = baseline === null ? [] : baseline.packages.filter((pkg) => !present.has(packageKey(pkg))).map((pkg) => ({
name: pkg.name,
ecosystem: pkg.ecosystem,
declared: pkg.declared,
installed: pkg.installed,
manifest: "",
dev: pkg.dev,
import_count: null,
unused: false,
status: "removed",
private: false
}));
const inventory = {
schema_version: 1,
generated_at: now.toISOString(),
run_id: input.runId,
packages: [...current, ...removed].sort(
(left, right) => left.ecosystem.localeCompare(right.ecosystem) || left.name.localeCompare(right.name) || left.manifest.localeCompare(right.manifest)
),
frameworks: input.frameworks ?? await detectFrameworks(input.projectRoot),
manifests: picks.map((pick) => pick.manifest),
manifests_looked_for: picks.length === 0 ? knownManifestNames() : [],
compared_to: baseline === null ? null : baselineLabel(baseline)
};
return inventory;
}
function baselineIndex(baseline) {
return new Map(baseline.packages.map((pkg) => [packageKey(pkg), pkg]));
}
async function writeStackInventory(input) {
const inventory = await buildStackInventory(input);
atomicWriteJson(absRunFile(input.projectRoot, input.runId, "inventory"), inventory);
return inventory;
}
function inventorySummary(inventory) {
if (inventory.manifests.length === 0) {
return `no package manifest found \u2014 looked for ${inventory.manifests_looked_for.join(", ")}. Rules discovery goes on with decisions and existing rules only.`;
}
const count = (status) => inventory.packages.filter((pkg) => pkg.status === status).length;
const listed = inventory.packages.filter((pkg) => pkg.status !== "removed").length;
return `${listed} packages from ${inventory.manifests.length} manifest${inventory.manifests.length === 1 ? "" : "s"}: ${count("new")} new, ${count("version-changed")} version-changed, ${count("removed")} removed${inventory.compared_to === null ? " (first run, no baseline)" : ` against ${inventory.compared_to}`}.`;
}
// src/rules-discovery/run-store.ts
init_esm_shims();
import { readdirSync } from "fs";
import { join as join8 } from "path";
var RUN_STEPS = [
"inventory",
"decisions",
"research",
"propose",
"sign_off",
"apply",
"docs",
"done"
];
var RUN_DOC_TYPE = "paqad.rules-discovery.run";
function stepIndex(step) {
return RUN_STEPS.indexOf(step);
}
function isRunRecord(value) {
if (typeof value !== "object" || value === null) return false;
const record = value;
return record.doc_type === RUN_DOC_TYPE && typeof record.run_id === "string" && typeof record.revision === "number" && typeof record.step === "string" && RUN_STEPS.includes(record.step) && (record.status === "open" || record.status === "done" || record.status === "abandoned");
}
function readRun(projectRoot, runId) {
if (!isRunId(runId)) {
return { state: "unreadable", reason: `"${runId}" is not a rules discovery run id` };
}
const read = readJsonFile(absRunFile(projectRoot, runId, "run"));
if (read.state === "missing") return { state: "missing" };
if (read.state === "unreadable") return { state: "unreadable", reason: read.error };
if (!isRunRecord(read.value)) {
return { state: "unreadable", reason: "run.json does not hold a rules discovery run" };
}
return { state: "ok", record: read.value };
}
function listRunIds(projectRoot) {
try {
return readdirSync(join8(projectRoot, RULES_DISCOVERY_DIR), { withFileTypes: true }).filter((entry) => entry.isDirectory() && isRunId(entry.name)).map((entry) => entry.name).sort();
} catch {
return [];
}
}
function findOpenRunId(projectRoot) {
const open = listRunIds(projectRoot).filter((runId) => {
const read = readRun(projectRoot, runId);
return read.state !== "ok" || read.record.status === "open";
});
return open.length === 0 ? null : open[open.length - 1];
}
function openRun(projectRoot, now = /* @__PURE__ */ new Date()) {
let runId = mintRunId(now);
for (let suffix = 2; readRun(projectRoot, runId).state !== "missing"; suffix += 1) {
runId = mintRunId(now, String(suffix));
}
const at = now.toISOString();
const record = {
schema_version: 1,
doc_type: RUN_DOC_TYPE,
run_id: runId,
revision: 1,
step: "inventory",
status: "open",
opened_at: at,
updated_at: at,
signoff_decision: null,
proposal_hash: null,
steps: [{ step: "inventory", at }]
};
atomicWriteJson(absRunFile(projectRoot, runId, "run"), record);
return record;
}
function updateRun(projectRoot, runId, expectedRevision, patch, now = /* @__PURE__ */ new Date()) {
const read = readRun(projectRoot, runId);
if (read.state === "missing") {
return { ok: false, reason: "missing", detail: `run ${runId} has no run.json` };
}
if (read.state === "unreadable") {
return { ok: false, reason: "unreadable", detail: read.reason };
}
const current = read.record;
if (current.revision !== expectedRevision) {
return {
ok: false,
reason: "revision-mismatch",
detail: `run ${runId} is at revision ${current.revision}, expected ${expectedRevision}; another session changed it`
};
}
if (current.status !== "open") {
return { ok: false, reason: "closed", detail: `run ${runId} is ${current.status}` };
}
if (patch.step !== void 0 && stepIndex(patch.step) < stepIndex(current.step)) {
return {
ok: false,
reason: "step-backwards",
detail: `run ${runId} is at ${current.step} and cannot go back to ${patch.step}`
};
}
const at = now.toISOString();
const step = patch.step ?? current.step;
const record = {
...current,
revision: current.revision + 1,
step,
status: patch.status ?? (step === "done" ? "done" : current.status),
updated_at: at,
signoff_decision: patch.signoff_decision === void 0 ? current.signoff_decision : patch.signoff_decision,
proposal_hash: patch.proposal_hash === void 0 ? current.proposal_hash : patch.proposal_hash,
steps: step === current.step ? current.steps : [...current.steps, { step, at }]
};
atomicWriteJson(absRunFile(projectRoot, runId, "run"), record);
return { ok: true, record };
}
// src/rules-discovery/nudge.ts
var NUDGE_NAME_LIMIT = 64;
var SHOWN_NAMES = 3;
var NAME_FORMATS = {
node: /^(?:@[a-z0-9~-][a-z0-9._~-]*\/)?[a-z0-9~-][a-z0-9._~-]*$/,
python: /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/,
php: /^[a-z0-9]+(?:[_.-][a-z0-9]+)*\/[a-z0-9]+(?:(?:[_.]|-{1,2})[a-z0-9]+)*$/,
go: /^[A-Za-z0-9][A-Za-z0-9._~/-]*$/,
rust: /^[A-Za-z0-9_-]+$/,
ruby: /^[A-Za-z0-9][A-Za-z0-9._-]*$/,
jvm: /^[A-Za-z0-9._-]+:[A-Za-z0-9._-]+$/,
dart: /^[a-z0-9_]+$/,
dotnet: /^[A-Za-z0-9_][A-Za-z0-9._-]*$/
};
function isShowablePackageName(ecosystem, name) {
return name.length <= NUDGE_NAME_LIMIT && NAME_FORMATS[ecosystem].test(name);
}
function nodeNamesInOrder(text) {
try {
const parsed = JSON.parse(text);
return ["dependencies", "devDependencies"].flatMap((field) => {
const block = parsed[field];
return block !== null && typeof block === "object" ? Object.keys(block) : [];
});
} catch {
return null;
}
}
function manifestPackageNames(projectRoot) {
const parsers = createDefaultEcosystemParserRegistry().list();
const patterns = Array.from(new Set(parsers.flatMap((parser) => parser.manifestFiles)));
const files = scanWorkingTree(
projectRoot,
patterns.map((pattern) => `**/${pattern}`),
DEFAULT_IGNORE_GLOBS
);
const seen = /* @__PURE__ */ new Set();
const out = [];
for (const file of files) {
const name = basename3(file);
const parser = parsers.find(
(candidate) => candidate.manifestFiles.some(
(pattern) => pattern.startsWith("*") ? name.endsWith(pattern.slice(1)) : name === pattern
)
);
if (parser === void 0) continue;
const text = readFileSync3(join9(projectRoot, file), "utf8");
const names = (name === "package.json" ? nodeNamesInOrder(text) : null) ?? parser.parseManifest(text, name).packages.map((pkg) => pkg.name);
for (const packageName of names) {
const entry = { name: packageName, ecosystem: parser.ecosystem };
if (!seen.has(packageKey(entry))) {
seen.add(packageKey(entry));
out.push(entry);
}
}
}
return out;
}
function nudgeLine(count, names, openRun2) {
const noun = count === 1 ? "new package" : "new packages";
const shown = names.length === 0 ? "" : ` (${names.join(", ")})`;
const ask = openRun2 ? "A rules discovery run is open: say 'discover rules' to continue it." : "Say 'discover rules' to update them.";
return `Your rules may be behind: ${count} ${noun}${shown}. ${ask}`;
}
function computeRulesDriftNudge(projectRoot, now = /* @__PURE__ */ new Date()) {
const baseline = readOrSeedBaseline(projectRoot, now);
if (baseline === null) return null;
const known = new Set(baseline.packages.map(packageKey));
const added = manifestPackageNames(projectRoot).filter((pkg) => !known.has(packageKey(pkg)));
if (added.length === 0) return null;
const names = added.filter((pkg) => isShowablePackageName(pkg.ecosystem, pkg.name)).slice(0, SHOWN_NAMES).map((pkg) => pkg.name);
const setHash = createHash("sha256").update(added.map(packageKey).sort().join("\n")).digest("hex");
const openRun2 = findOpenRunId(projectRoot);
return {
line: nudgeLine(added.length, names, openRun2),
set_hash: setHash,
count: added.length,
names,
open_run: openRun2
};
}
function readNudgeState(projectRoot) {
const read = readJsonFile(join9(projectRoot, RULES_DISCOVERY_NUDGE_STATE));
return read.state === "ok" && typeof read.value.set_hash === "string" ? read.value : null;
}
function clearNudgeState(projectRoot) {
atomicWriteJson(join9(projectRoot, RULES_DISCOVERY_NUDGE_STATE), {
set_hash: "",
shown_at: "",
line: ""
});
}
function rulesDriftSessionStartText(projectRoot, now = /* @__PURE__ */ new Date()) {
const nudge = computeRulesDriftNudge(projectRoot, now);
const state = readNudgeState(projectRoot);
if (nudge === null) {
if (state !== null && state.line !== "") clearNudgeState(projectRoot);
return null;
}
const key = `${nudge.set_hash}:${nudge.open_run ?? ""}`;
if (state?.set_hash === key) return null;
atomicWriteJson(join9(projectRoot, RULES_DISCOVERY_NUDGE_STATE), {
set_hash: key,
shown_at: now.toISOString(),
line: nudge.line
});
return nudge.line;
}
function rulesDriftHeadline(projectRoot) {
const state = readNudgeState(projectRoot);
return state === null || state.line === "" ? null : state.line;
}
export {
prefixRepositoryPath,
jvmParser,
EcosystemParserRegistry,
expandManifestNames,
createDefaultEcosystemParserRegistry,
detectEnvironmentTraits,
StackIntrospector,
atomicWriteText,
atomicWriteJson,
readJsonFile,
RULES_ROOT,
PROJECT_RULES_DIR,
isRunId,
mintRunId,
runFilePath,
absRunFile,
moveBaselineToRun,
packageKey,
writeStackInventory,
inventorySummary,
RUN_DOC_TYPE,
readRun,
findOpenRunId,
openRun,
updateRun,
NUDGE_NAME_LIMIT,
isShowablePackageName,
manifestPackageNames,
nudgeLine,
computeRulesDriftNudge,
readNudgeState,
clearNudgeState,
rulesDriftSessionStartText,
rulesDriftHeadline
};