feat: add circular dep detection tool + fix duplicate milestone dirs + fix metrics NULL
- Add scripts/check-circular-deps.mjs using madge; npm run check:circular and check:circular:ext scan src/ and the SF extension respectively - findMilestoneIds() is now DB-first: reads from milestones table when DB is open so stale/duplicate filesystem dirs (M001/ and M001-6377a4/) are never returned; falls back to fs scan only during early bootstrap - milestone-id-utils.js was a stale duplicate; replaced with re-exports from canonical milestone-ids.js - metrics-central.js: guard null/undefined counter/gauge/histogram values with ?? 0 to prevent NOT NULL constraint failure on metrics.value Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
15185c2e7d
commit
ea360f6ad2
6 changed files with 1701 additions and 225 deletions
1419
package-lock.json
generated
1419
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -109,7 +109,9 @@
|
|||
"docker:build-runtime": "docker build --target runtime -t ghcr.io/singularity-ng/singularity-forge .",
|
||||
"docker:build-builder": "docker build --target builder -t ghcr.io/singularity-forge/sf-ci-builder .",
|
||||
"prepublishOnly": "npm run sync-pkg-version && npm run sync-platform-versions && node scripts/prepublish-check.mjs && npm run build && npm run typecheck:extensions && npm run validate-pack",
|
||||
"test:live-regression": "node --experimental-strip-types tests/live-regression/run.ts"
|
||||
"test:live-regression": "node --experimental-strip-types tests/live-regression/run.ts",
|
||||
"check:circular": "node scripts/check-circular-deps.mjs",
|
||||
"check:circular:ext": "node scripts/check-circular-deps.mjs --ext"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.95.1",
|
||||
|
|
@ -172,6 +174,7 @@
|
|||
"esbuild": "^0.27.7",
|
||||
"jiti": "^2.7.0",
|
||||
"jscpd": "^4.0.9",
|
||||
"madge": "^8.0.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-language-server": "^5.1.3",
|
||||
"vitest": "^4.1.5"
|
||||
|
|
|
|||
62
scripts/check-circular-deps.mjs
Normal file
62
scripts/check-circular-deps.mjs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-circular-deps.mjs — detect circular imports across the SF codebase.
|
||||
*
|
||||
* Usage:
|
||||
* npm run check:circular # scan src/ + packages/
|
||||
* npm run check:circular -- --ext # scan extension source only
|
||||
* node scripts/check-circular-deps.mjs [--ext] [--json]
|
||||
*
|
||||
* Exit 0 = no cycles found. Exit 1 = cycles detected (or scan error).
|
||||
*/
|
||||
|
||||
import madge from "madge";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(__dirname, "..");
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const extOnly = args.includes("--ext");
|
||||
const jsonOut = args.includes("--json");
|
||||
|
||||
const entries = extOnly
|
||||
? [resolve(root, "src/resources/extensions/sf")]
|
||||
: [resolve(root, "src"), resolve(root, "packages")];
|
||||
|
||||
console.error(`Scanning: ${entries.map((e) => e.replace(root + "/", "")).join(", ")}`);
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await madge(entries, {
|
||||
fileExtensions: ["js", "mjs", "ts"],
|
||||
excludeRegExp: [
|
||||
/node_modules/,
|
||||
/\.test\.(js|mjs|ts)$/,
|
||||
/\/dist\//,
|
||||
/\/tests?\//,
|
||||
],
|
||||
detectiveOptions: {
|
||||
es6: { mixedImports: true },
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Scan failed: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const cycles = result.circular();
|
||||
|
||||
if (jsonOut) {
|
||||
console.log(JSON.stringify({ cycles, count: cycles.length }, null, 2));
|
||||
} else if (cycles.length === 0) {
|
||||
console.log("✅ No circular dependencies found.");
|
||||
} else {
|
||||
console.log(`❌ ${cycles.length} circular dependency chain(s) found:\n`);
|
||||
for (const [i, chain] of cycles.entries()) {
|
||||
console.log(` ${i + 1}. ${chain.join(" → ")} → ${chain[0]}`);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(cycles.length > 0 ? 1 : 0);
|
||||
|
|
@ -469,7 +469,7 @@ function persistMetricsToDb(registry, sessionId, _ignored) {
|
|||
c.name,
|
||||
"counter",
|
||||
JSON.stringify(labels),
|
||||
value,
|
||||
value ?? 0,
|
||||
ts,
|
||||
sessionId,
|
||||
);
|
||||
|
|
@ -482,7 +482,7 @@ function persistMetricsToDb(registry, sessionId, _ignored) {
|
|||
g.name,
|
||||
"gauge",
|
||||
JSON.stringify(labels),
|
||||
value,
|
||||
value ?? 0,
|
||||
ts,
|
||||
sessionId,
|
||||
);
|
||||
|
|
@ -493,7 +493,7 @@ function persistMetricsToDb(registry, sessionId, _ignored) {
|
|||
h.name,
|
||||
"histogram",
|
||||
JSON.stringify({ count: h.count, sum: h.sum }),
|
||||
h.sum,
|
||||
h.sum ?? 0,
|
||||
ts,
|
||||
sessionId,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,27 +1,10 @@
|
|||
import { readdirSync } from "node:fs";
|
||||
import { milestonesDir } from "./paths.js";
|
||||
/** Matches both classic `M001` and unique `M001-abc123` formats (anchored). */
|
||||
export const MILESTONE_ID_RE = /^M\d{3}(?:-[a-z0-9]{6})?$/;
|
||||
/** Extract the trailing sequential number from a milestone ID. Returns 0 for non-matches. */
|
||||
export function extractMilestoneSeq(id) {
|
||||
const match = id.match(/^M(\d{3})(?:-[a-z0-9]{6})?$/);
|
||||
return match ? parseInt(match[1], 10) : 0;
|
||||
}
|
||||
/** Comparator for sorting milestone IDs by sequential number. */
|
||||
export function milestoneIdSort(a, b) {
|
||||
return extractMilestoneSeq(a) - extractMilestoneSeq(b);
|
||||
}
|
||||
export function findMilestoneIds(basePath) {
|
||||
const dir = milestonesDir(basePath);
|
||||
try {
|
||||
return readdirSync(dir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => {
|
||||
const match = entry.name.match(/^(M\d+(?:-[a-z0-9]{6})?)/);
|
||||
return match ? match[1] : entry.name;
|
||||
})
|
||||
.sort(milestoneIdSort);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Re-exports from the canonical milestone-ids.js.
|
||||
* This file exists for backwards compatibility with older import paths.
|
||||
*/
|
||||
export {
|
||||
MILESTONE_ID_RE,
|
||||
extractMilestoneSeq,
|
||||
milestoneIdSort,
|
||||
findMilestoneIds,
|
||||
} from "./milestone-ids.js";
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { getErrorMessage } from "./error-utils.js";
|
|||
import { milestonesDir } from "./paths.js";
|
||||
import { loadQueueOrder, sortByQueueOrder } from "./queue-order.js";
|
||||
import { logWarning } from "./workflow-logger.js";
|
||||
import { getAllMilestones } from "./sf-db.js";
|
||||
// ─── Regex ──────────────────────────────────────────────────────────────────
|
||||
/** Matches both classic `M001` and unique `M001-abc123` formats (anchored). */
|
||||
export const MILESTONE_ID_RE = /^M\d{3}(?:-[a-z0-9]{6})?$/;
|
||||
|
|
@ -91,8 +92,28 @@ export function clearReservedMilestoneIds() {
|
|||
reservedMilestoneIds.clear();
|
||||
}
|
||||
// ─── Discovery ──────────────────────────────────────────────────────────────
|
||||
/** Scan the milestones directory and return IDs sorted by queue order (or numeric fallback). */
|
||||
/**
|
||||
* Return milestone IDs, DB-first.
|
||||
*
|
||||
* When the DB is open, reads from the `milestones` table — the canonical
|
||||
* source of truth — so stale or duplicated filesystem dirs (e.g. both
|
||||
* `M001/` and `M001-6377a4/`) are never returned.
|
||||
* Falls back to a filesystem scan only when the DB is not yet open (early
|
||||
* bootstrap or first-init before `ensureDbOpen`).
|
||||
*/
|
||||
export function findMilestoneIds(basePath) {
|
||||
// DB-first: avoids returning duplicate/legacy dirs from filesystem
|
||||
try {
|
||||
const dbRows = getAllMilestones();
|
||||
if (dbRows.length > 0) {
|
||||
const ids = dbRows.map((m) => m.id);
|
||||
const customOrder = loadQueueOrder(basePath);
|
||||
return sortByQueueOrder(ids, customOrder);
|
||||
}
|
||||
} catch {
|
||||
// DB not open yet — fall through to filesystem scan
|
||||
}
|
||||
// Filesystem fallback (early bootstrap / first-init)
|
||||
const dir = milestonesDir(basePath);
|
||||
try {
|
||||
const ids = readdirSync(dir, { withFileTypes: true })
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue