connections-architect 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/architect.config.example.json +17 -0
- package/bin/architect.mjs +80 -0
- package/package.json +31 -0
- package/spine/justify-existence.example.json +11 -0
- package/src/checks/code/dependency-freshness.mjs +87 -0
- package/src/checks/code/no-secrets-committed.mjs +65 -0
- package/src/checks/code/oversized-files.mjs +40 -0
- package/src/checks/code/sibling-consensus.mjs +73 -0
- package/src/checks/code/tsconfig-conformance.mjs +105 -0
- package/src/checks/hosted/db-justify-existence.mjs +113 -0
- package/src/core/discovery.mjs +36 -0
- package/src/core/finding.mjs +40 -0
- package/src/core/fs-walk.mjs +22 -0
- package/src/core/hosted/judge.mjs +45 -0
- package/src/core/hosted/spine.mjs +42 -0
- package/src/core/hosted/vault-exec.mjs +61 -0
- package/src/core/index.mjs +12 -0
- package/src/core/oracle/family-conformance.mjs +83 -0
- package/src/core/project-detect.mjs +106 -0
- package/src/core/runner.mjs +140 -0
- package/src/core/sarif.mjs +81 -0
- package/src/update/publish-core.mjs +93 -0
- package/src/update/self-update.mjs +90 -0
- package/your-checks/README.md +27 -0
- package/your-checks/code/example-check.mjs +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# connections-architect
|
|
2
|
+
|
|
3
|
+
The **Architect** — a portable checks-and-balances framework. A generic CORE of checks ships from
|
|
4
|
+
Connections; you add your OWN under `your-checks/` (the core never touches them); together they form a
|
|
5
|
+
**cast** on your codebase that catches breakage and drift and gets tighter as you add rules.
|
|
6
|
+
|
|
7
|
+
> Built from scratch, distilling the best of Connections' internal `arkitect` (the self-registering check
|
|
8
|
+
> contract, two-root auto-discovery, the `requires` portability gate, the conformance-oracle tier) and
|
|
9
|
+
> `daggar` (the multi-axis "justify existence" judge for live infra/DB). Zero runtime dependencies.
|
|
10
|
+
|
|
11
|
+
## Use it
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npx -y connections-architect # run every applicable check
|
|
15
|
+
npx -y connections-architect --check no-secrets-committed
|
|
16
|
+
npx -y connections-architect list # what's discovered (core + your-checks)
|
|
17
|
+
npx -y connections-architect --fail-on-drift # exit 1 if a gating check fails (CI)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Output: `tmp/architect/FIX_QUEUE.md` (ranked) + `tmp/architect/manifest.json` whose `pass.nextAction`
|
|
21
|
+
(`fix-errors` → `review-warnings` → `done`) is the one field an agent reads to know if it's finished.
|
|
22
|
+
|
|
23
|
+
## Two halves, one shape
|
|
24
|
+
|
|
25
|
+
- **code** — checks that run statically in your repo (shipped now).
|
|
26
|
+
- **hosted** — checks that run through the **Connections MCP** against _your own_ cloud credentials
|
|
27
|
+
(AWS / Supabase / your DB), generalizing the daggar DB governor's "justify existence" judgment.
|
|
28
|
+
Strictly read-only; emits a `remediation.sql` / kill-orders a human executes. _(Phase 3.)_
|
|
29
|
+
|
|
30
|
+
## Make it yours
|
|
31
|
+
|
|
32
|
+
Add `.mjs` files under `your-checks/` — each exports an `audit` and **auto-registers, no wiring**. A user
|
|
33
|
+
check whose `id` matches a core check **shadows** it (patch the core without editing it). Pin or fork via
|
|
34
|
+
`architect.config.json` (`update.pinnedVersion` / `update.autoUpdate: false`) to stop pulling the core and
|
|
35
|
+
own it outright.
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
export const audit = {
|
|
39
|
+
id: "my-rule",
|
|
40
|
+
title: "My rule",
|
|
41
|
+
category: "custom",
|
|
42
|
+
domain: "code", // or "hosted"
|
|
43
|
+
requires: {}, // {} = any repo
|
|
44
|
+
gating: false, // true ⇒ blocks --fail-on-drift
|
|
45
|
+
async run(ctx) {
|
|
46
|
+
return { failed: false, findings: [], report: "" };
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Node ≥ 18. `node test/smoke.mjs` proves the engine end to end.
|
|
52
|
+
|
|
53
|
+
## Roadmap
|
|
54
|
+
|
|
55
|
+
| Phase | What |
|
|
56
|
+
| ----- | ---- |
|
|
57
|
+
| **1 (done)** | portable core engine (runner + two-root discovery + `requires` gating + FIX_QUEUE/manifest) + 3 generic checks + the `your-checks/` extension layer |
|
|
58
|
+
| **2 (done)** | conformance-oracle reference mode (`runFamilyConformance` — reference + consensus), SARIF 2.1.0 (`findings.sarif`), DECISION_BRIEF flat finding list, `tsconfig-conformance` check, `dependency-freshness` check |
|
|
59
|
+
| **3 (done)** | the hosted/DB half — generic 5-verdict justify-existence judge + spine (claims/overrides) + read-only vault seam + the `db-justify-existence` governor (runs through the MCP vault against your own DB; review-only remediation) |
|
|
60
|
+
| **4 (done, unreleased)** | self-update machinery — version-gated manifest pull + SHA-pinned apply with `.prev` rollback (mirrors the MCP loader) + `publish-core.mjs` release publisher. Built + verified; **not published** (release-time) |
|
|
61
|
+
| **5 (wired; awaiting release)** | the MCP `architect_run` tool (code half + hosted-via-vault) is built. The public releases — npm publish + S3 core publish + the MCP deploy — are **owner-gated**, not done yet |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"extensionDir": "your-checks",
|
|
4
|
+
"update": {
|
|
5
|
+
"channel": "stable",
|
|
6
|
+
"pinnedVersion": null,
|
|
7
|
+
"autoUpdate": true
|
|
8
|
+
},
|
|
9
|
+
"checks": {
|
|
10
|
+
"oversized-files": { "warnLines": 800, "errorLines": 2500 },
|
|
11
|
+
"sibling-consensus": { "family": "package.json", "field": "license" }
|
|
12
|
+
},
|
|
13
|
+
"hosted": {
|
|
14
|
+
"spine": "spine/justify-existence.json",
|
|
15
|
+
"targets": []
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The Architect CLI. `architect` / `architect --all` runs every applicable check; `architect --check <id>`
|
|
3
|
+
// runs one; `architect list` shows what's discovered. It discovers the shipped CORE (this package's
|
|
4
|
+
// src/checks/) AND the target repo's your-checks/ — the user's checks shadow core checks of the same id.
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { discoverAudits } from "../src/core/discovery.mjs";
|
|
9
|
+
import { detectProject } from "../src/core/project-detect.mjs";
|
|
10
|
+
import { runAudits } from "../src/core/runner.mjs";
|
|
11
|
+
import { selfUpdate } from "../src/update/self-update.mjs";
|
|
12
|
+
|
|
13
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const PKG = resolve(HERE, "..");
|
|
15
|
+
const argv = process.argv.slice(2);
|
|
16
|
+
const flag = (f) => argv.includes(f);
|
|
17
|
+
const opt = (f, d) => {
|
|
18
|
+
const i = argv.indexOf(f);
|
|
19
|
+
return i >= 0 && argv[i + 1] ? argv[i + 1] : d;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const root = resolve(opt("--root", process.cwd()));
|
|
23
|
+
const failOnDrift = flag("--fail-on-drift");
|
|
24
|
+
const only = opt("--check", null);
|
|
25
|
+
const wantList = argv.includes("list") || flag("--list");
|
|
26
|
+
|
|
27
|
+
// Config (optional) lives in the TARGET repo — never in the core.
|
|
28
|
+
let config = {};
|
|
29
|
+
const cfgPath = join(root, "architect.config.json");
|
|
30
|
+
if (existsSync(cfgPath)) {
|
|
31
|
+
try {
|
|
32
|
+
config = JSON.parse(readFileSync(cfgPath, "utf8"));
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.error(`[architect] bad architect.config.json: ${e.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const extensionDir = config.extensionDir || "your-checks";
|
|
38
|
+
const coreRoot = join(PKG, "src", "checks"); // shipped CORE — what self-update refreshes
|
|
39
|
+
const userRoot = join(root, extensionDir); // the user's own checks — never touched by self-update
|
|
40
|
+
|
|
41
|
+
// Pull the freshest CORE from Connections before discovery — opt-in via config.update.autoUpdate (the
|
|
42
|
+
// shipped default config sets it true). Fail-soft: an update attempt NEVER breaks the run. No config
|
|
43
|
+
// opt-in ⇒ no network (so dev + CI stay offline). Inert until the core bucket exists at release.
|
|
44
|
+
if (config.update?.autoUpdate === true && !config.update?.pinnedVersion) {
|
|
45
|
+
try {
|
|
46
|
+
const installedVersion = JSON.parse(readFileSync(join(PKG, "package.json"), "utf8")).version;
|
|
47
|
+
const r = await selfUpdate({ installedVersion, srcDir: join(PKG, "src"), config });
|
|
48
|
+
if (r?.applied) console.error(`[architect] self-updated core → v${r.version} (${r.applied} files)`);
|
|
49
|
+
} catch {
|
|
50
|
+
/* an update attempt must never break a run */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { audits, loadErrors, shadowed } = await discoverAudits([coreRoot, userRoot]);
|
|
55
|
+
for (const le of loadErrors) console.error(`[architect] FAILED TO LOAD check ${le.file}: ${le.error}`);
|
|
56
|
+
for (const s of shadowed) console.error(`[architect] your-checks override: '${s.id}' shadows the core check`);
|
|
57
|
+
|
|
58
|
+
if (wantList) {
|
|
59
|
+
for (const a of audits.sort((x, y) => x.id.localeCompare(y.id))) {
|
|
60
|
+
const where = a.__root === coreRoot ? "core" : "your-checks";
|
|
61
|
+
console.log(`${a.id.padEnd(28)} [${a.__group}/${where}] ${a.title}`);
|
|
62
|
+
}
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const selected = only ? audits.filter((a) => a.id === only) : audits;
|
|
67
|
+
if (only && selected.length === 0) {
|
|
68
|
+
console.error(`[architect] no check with id '${only}'. Try: architect list`);
|
|
69
|
+
process.exit(2);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const project = detectProject(root);
|
|
73
|
+
const ctxBase = { root, config, project, options: { failOnDrift }, vault: null };
|
|
74
|
+
const { manifest } = await runAudits(selected, ctxBase, { outDir: join(root, "tmp", "architect") });
|
|
75
|
+
|
|
76
|
+
const line = `Architect · ${manifest.checks.length} checks · ${manifest.gatingErrors} gating error(s) · ${manifest.warnings} warning(s) · ${manifest.crashed} crash(es) · nextAction=${manifest.pass.nextAction}`;
|
|
77
|
+
console.log(manifest.pass.clean ? `✅ ${line}` : `⚠️ ${line}`);
|
|
78
|
+
console.log(" → tmp/architect/FIX_QUEUE.md · tmp/architect/manifest.json");
|
|
79
|
+
|
|
80
|
+
process.exit(failOnDrift && manifest.gatingErrors > 0 ? 1 : 0);
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "connections-architect",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "The Connections Architect — a portable, self-updating checks-and-balances framework. Ships a generic core of checks; you add your own in your-checks/ (the core never touches them); it keeps your codebase (and, via the Connections MCP, your hosted cloud) from drifting. Zero runtime dependencies.",
|
|
6
|
+
"bin": {
|
|
7
|
+
"architect": "./bin/architect.mjs"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/core/index.mjs",
|
|
11
|
+
"./finding": "./src/core/finding.mjs",
|
|
12
|
+
"./oracle": "./src/core/oracle/family-conformance.mjs"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"src",
|
|
17
|
+
"your-checks",
|
|
18
|
+
"spine",
|
|
19
|
+
"architect.config.example.json",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"keywords": ["lint", "audit", "checks", "governance", "drift", "connections", "self-updating"],
|
|
23
|
+
"homepage": "https://studio.connections.icu/mcp",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"license": "UNLICENSED"
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"appCodeRoots": ["src", "services", "lib"],
|
|
3
|
+
"features": [
|
|
4
|
+
{ "name": "billing", "claims": ["invoices", "payments", "payment_*"] },
|
|
5
|
+
{ "name": "identity", "claims": ["users", "sessions", "profile_*"] }
|
|
6
|
+
],
|
|
7
|
+
"verdicts": {
|
|
8
|
+
"legacy_temp_import": "REMOVE",
|
|
9
|
+
"mystery_table": "INVESTIGATE"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// CORE check — advisory. Flags package.json files that depend on unpinned version ranges (`*`, `latest`,
|
|
2
|
+
// `>=x`) or that have dependencies declared but no lockfile in the same directory. Unpinned ranges mean
|
|
3
|
+
// your build is not reproducible; missing lockfiles mean you can't prove it either. Advisory by default
|
|
4
|
+
// so it never blocks a fork whose own dep conventions differ.
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { relative, basename, dirname, join } from "node:path";
|
|
7
|
+
import { createFinding } from "../../core/finding.mjs";
|
|
8
|
+
import { walkFiles } from "../../core/fs-walk.mjs";
|
|
9
|
+
|
|
10
|
+
/** Unpinned range patterns — `*`, `latest`, `>=x`, `>x`. Does NOT flag `^` / `~` (those are pinned ranges). */
|
|
11
|
+
const UNPINNED_RE = /^(\*|latest|>=?)/;
|
|
12
|
+
|
|
13
|
+
/** Lockfile names we recognize. */
|
|
14
|
+
const LOCKFILES = ["package-lock.json", "bun.lockb", "yarn.lock", "pnpm-lock.yaml"];
|
|
15
|
+
|
|
16
|
+
export const audit = {
|
|
17
|
+
id: "dependency-freshness",
|
|
18
|
+
title: "Dependencies use pinned version ranges",
|
|
19
|
+
category: "dependencies",
|
|
20
|
+
domain: "code",
|
|
21
|
+
requires: { ecosystems: ["npm"] },
|
|
22
|
+
gating: false,
|
|
23
|
+
defaultConfig: {},
|
|
24
|
+
async run(ctx) {
|
|
25
|
+
const { root } = ctx;
|
|
26
|
+
const findings = [];
|
|
27
|
+
|
|
28
|
+
for (const file of walkFiles(root)) {
|
|
29
|
+
if (basename(file) !== "package.json") continue;
|
|
30
|
+
|
|
31
|
+
let pkg;
|
|
32
|
+
try {
|
|
33
|
+
pkg = JSON.parse(readFileSync(file, "utf8"));
|
|
34
|
+
} catch {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const relFile = relative(root, file);
|
|
39
|
+
const dir = dirname(file);
|
|
40
|
+
|
|
41
|
+
// 1. Check for unpinned ranges in dependencies + devDependencies.
|
|
42
|
+
const allDeps = {
|
|
43
|
+
...(pkg.dependencies || {}),
|
|
44
|
+
...(pkg.devDependencies || {}),
|
|
45
|
+
};
|
|
46
|
+
for (const [name, version] of Object.entries(allDeps)) {
|
|
47
|
+
if (typeof version === "string" && UNPINNED_RE.test(version.trim())) {
|
|
48
|
+
findings.push(
|
|
49
|
+
createFinding({
|
|
50
|
+
id: "unpinned-dependency",
|
|
51
|
+
title: `Unpinned dependency: ${name}`,
|
|
52
|
+
severity: "warning",
|
|
53
|
+
file: relFile,
|
|
54
|
+
message: `'${name}': "${version}" is unpinned (use an exact version or a caret/tilde range for reproducibility)`,
|
|
55
|
+
fix: `Replace "${version}" with a pinned version such as "1.2.3" or "^1.2.3".`,
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. Check for missing lockfile when any deps are declared.
|
|
62
|
+
const hasDeps =
|
|
63
|
+
Object.keys(pkg.dependencies || {}).length > 0 ||
|
|
64
|
+
Object.keys(pkg.devDependencies || {}).length > 0;
|
|
65
|
+
if (hasDeps) {
|
|
66
|
+
const hasLockfile = LOCKFILES.some((lf) => existsSync(join(dir, lf)));
|
|
67
|
+
if (!hasLockfile) {
|
|
68
|
+
findings.push(
|
|
69
|
+
createFinding({
|
|
70
|
+
id: "missing-lockfile",
|
|
71
|
+
title: "No lockfile found alongside package.json",
|
|
72
|
+
severity: "warning",
|
|
73
|
+
file: relFile,
|
|
74
|
+
message: `${relFile} declares dependencies but has no lockfile (${LOCKFILES.join(" / ")}) — installs are not reproducible`,
|
|
75
|
+
fix: "Run your package manager (npm install / bun install / yarn / pnpm install) to generate a lockfile and commit it.",
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const report = findings.length
|
|
83
|
+
? `# Dependency freshness\n\n${findings.map((f) => `- WARN ${f.file} — ${f.message}`).join("\n")}\n\nErrors: 0\nWarnings: ${findings.length}\n`
|
|
84
|
+
: "";
|
|
85
|
+
return { failed: false, findings, report };
|
|
86
|
+
},
|
|
87
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// CORE check — generic, runs on any repo. High-confidence committed-secret patterns only (low false
|
|
2
|
+
// positives by design; broader entropy scanning is a Phase-2 refinement).
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { relative, extname } from "node:path";
|
|
5
|
+
import { createFinding } from "../../core/finding.mjs";
|
|
6
|
+
import { walkFiles } from "../../core/fs-walk.mjs";
|
|
7
|
+
|
|
8
|
+
const SCAN_EXT = new Set([
|
|
9
|
+
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".rs", ".go", ".rb", ".java", ".php",
|
|
10
|
+
".vue", ".json", ".yaml", ".yml", ".env", ".sh", ".toml", ".ini", ".cfg", ".tf",
|
|
11
|
+
]);
|
|
12
|
+
const PATTERNS = [
|
|
13
|
+
{ id: "aws-access-key-id", re: /\bAKIA[0-9A-Z]{16}\b/, title: "AWS access key id committed" },
|
|
14
|
+
{ id: "private-key-block", re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/, title: "Private key block committed" },
|
|
15
|
+
{ id: "aws-secret-key", re: /aws_secret_access_key["'\s:=]+["'][A-Za-z0-9/+=]{40}["']/i, title: "AWS secret access key committed" },
|
|
16
|
+
{ id: "slack-token", re: /\bxox[baprs]-[0-9A-Za-z-]{10,}\b/, title: "Slack token committed" },
|
|
17
|
+
{ id: "google-api-key", re: /\bAIza[0-9A-Za-z_\-]{35}\b/, title: "Google API key committed" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export const audit = {
|
|
21
|
+
id: "no-secrets-committed",
|
|
22
|
+
title: "No committed secrets",
|
|
23
|
+
category: "security",
|
|
24
|
+
domain: "code",
|
|
25
|
+
requires: {}, // any repo
|
|
26
|
+
gating: true,
|
|
27
|
+
defaultConfig: { maxFileBytes: 2_000_000 },
|
|
28
|
+
async run(ctx) {
|
|
29
|
+
const { root, checkConfig } = ctx;
|
|
30
|
+
const findings = [];
|
|
31
|
+
for (const file of walkFiles(root)) {
|
|
32
|
+
if (!SCAN_EXT.has(extname(file))) continue;
|
|
33
|
+
let text;
|
|
34
|
+
try {
|
|
35
|
+
const buf = readFileSync(file);
|
|
36
|
+
if (buf.length > checkConfig.maxFileBytes) continue;
|
|
37
|
+
text = buf.toString("utf8");
|
|
38
|
+
} catch {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const lines = text.split("\n");
|
|
42
|
+
for (let i = 0; i < lines.length; i++) {
|
|
43
|
+
for (const p of PATTERNS) {
|
|
44
|
+
if (p.re.test(lines[i])) {
|
|
45
|
+
findings.push(
|
|
46
|
+
createFinding({
|
|
47
|
+
id: p.id,
|
|
48
|
+
title: p.title,
|
|
49
|
+
severity: "error",
|
|
50
|
+
file: relative(root, file),
|
|
51
|
+
line: i + 1,
|
|
52
|
+
message: `${p.title} at ${relative(root, file)}:${i + 1}`,
|
|
53
|
+
fix: "Remove it, rotate the credential, and load it from an env var / secret manager.",
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const report = findings.length
|
|
61
|
+
? `# No committed secrets\n\n${findings.map((f) => `- ERROR ${f.file}:${f.line} — ${f.title}`).join("\n")}\n\nErrors: ${findings.length}\nWarnings: 0\n`
|
|
62
|
+
: "";
|
|
63
|
+
return { failed: findings.length > 0, findings, report };
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// CORE check — generic. Advisory by default (surfaces debt without blocking a fork whose files are
|
|
2
|
+
// already large). A user can flip it to gating in architect.config.json.
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { relative, extname } from "node:path";
|
|
5
|
+
import { createFinding } from "../../core/finding.mjs";
|
|
6
|
+
import { walkFiles } from "../../core/fs-walk.mjs";
|
|
7
|
+
|
|
8
|
+
const CODE_EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".rs", ".go", ".rb", ".java", ".php", ".vue", ".css", ".scss"]);
|
|
9
|
+
|
|
10
|
+
export const audit = {
|
|
11
|
+
id: "oversized-files",
|
|
12
|
+
title: "Oversized source files",
|
|
13
|
+
category: "maintainability",
|
|
14
|
+
domain: "code",
|
|
15
|
+
requires: {},
|
|
16
|
+
gating: false,
|
|
17
|
+
defaultConfig: { warnLines: 800, errorLines: 2500 },
|
|
18
|
+
async run(ctx) {
|
|
19
|
+
const { root, checkConfig } = ctx;
|
|
20
|
+
const findings = [];
|
|
21
|
+
for (const file of walkFiles(root)) {
|
|
22
|
+
if (!CODE_EXT.has(extname(file))) continue;
|
|
23
|
+
let lines;
|
|
24
|
+
try {
|
|
25
|
+
lines = readFileSync(file, "utf8").split("\n").length;
|
|
26
|
+
} catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (lines >= checkConfig.errorLines)
|
|
30
|
+
findings.push(createFinding({ id: "oversized", title: "File far too large", severity: "error", file: relative(root, file), line: lines, message: `${lines} lines (≥ ${checkConfig.errorLines})` }));
|
|
31
|
+
else if (lines >= checkConfig.warnLines)
|
|
32
|
+
findings.push(createFinding({ id: "oversized", title: "Large file", severity: "warning", file: relative(root, file), line: lines, message: `${lines} lines (≥ ${checkConfig.warnLines})` }));
|
|
33
|
+
}
|
|
34
|
+
const errors = findings.filter((f) => f.severity === "error").length;
|
|
35
|
+
const report = findings.length
|
|
36
|
+
? `# Oversized files\n\n${findings.map((f) => `- ${f.severity.toUpperCase()} ${f.file} — ${f.message}`).join("\n")}\n\nErrors: ${errors}\nWarnings: ${findings.length - errors}\n`
|
|
37
|
+
: "";
|
|
38
|
+
return { failed: errors > 0, findings, report };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// CORE check — the CONFORMANCE ORACLE (consensus mode), distilled to a generic, project-agnostic shape.
|
|
2
|
+
// A denylist enumerates known-bad and is forever one bug behind. An oracle declares what AGREEMENT looks
|
|
3
|
+
// like across a FAMILY of sibling artifacts and flags the odd-one-out — catching divergence nobody named.
|
|
4
|
+
// Default family: every package.json; default fingerprint: the `license` field. Both are configurable.
|
|
5
|
+
// Refactored to delegate consensus logic to runFamilyConformance (keeps identical behavior + output).
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { relative, basename } from "node:path";
|
|
8
|
+
import { createFinding } from "../../core/finding.mjs";
|
|
9
|
+
import { walkFiles } from "../../core/fs-walk.mjs";
|
|
10
|
+
import { runFamilyConformance } from "../../core/oracle/family-conformance.mjs";
|
|
11
|
+
|
|
12
|
+
function getPath(obj, path) {
|
|
13
|
+
return path.split(".").reduce((o, k) => (o == null ? undefined : o[k]), obj);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const audit = {
|
|
17
|
+
id: "sibling-consensus",
|
|
18
|
+
title: "Sibling files agree (consensus oracle)",
|
|
19
|
+
category: "consistency",
|
|
20
|
+
domain: "code",
|
|
21
|
+
requires: { ecosystems: ["npm"] },
|
|
22
|
+
gating: false,
|
|
23
|
+
defaultConfig: { family: "package.json", field: "license", minFamily: 3 },
|
|
24
|
+
async run(ctx) {
|
|
25
|
+
const { root, checkConfig } = ctx;
|
|
26
|
+
|
|
27
|
+
// Collect family members.
|
|
28
|
+
const members = [];
|
|
29
|
+
for (const file of walkFiles(root)) {
|
|
30
|
+
if (basename(file) === checkConfig.family) members.push({ path: file });
|
|
31
|
+
}
|
|
32
|
+
if (members.length < checkConfig.minFamily) return { failed: false, findings: [], report: "" };
|
|
33
|
+
|
|
34
|
+
// project() extracts the fingerprint for a member (the configured field from the JSON file).
|
|
35
|
+
const project = (member) => {
|
|
36
|
+
try {
|
|
37
|
+
return getPath(JSON.parse(readFileSync(member.path, "utf8")), checkConfig.field) ?? null;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const { findings: divergences, consensusOrReference } = await runFamilyConformance({
|
|
44
|
+
family: members,
|
|
45
|
+
project,
|
|
46
|
+
mode: "consensus",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (divergences === null || consensusOrReference === null) {
|
|
50
|
+
// No clear majority — nothing to flag.
|
|
51
|
+
return { failed: false, findings: [], report: "" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const majorityKey = JSON.stringify(consensusOrReference);
|
|
55
|
+
const majorityCount = members.length - divergences.length;
|
|
56
|
+
|
|
57
|
+
const findings = divergences.map((d) =>
|
|
58
|
+
createFinding({
|
|
59
|
+
id: "consensus-divergence",
|
|
60
|
+
title: `Diverges from sibling consensus on '${checkConfig.field}'`,
|
|
61
|
+
severity: "warning",
|
|
62
|
+
file: relative(root, d.member.path),
|
|
63
|
+
message: `'${checkConfig.field}' = ${JSON.stringify(d.actual)}; ${majorityCount}/${members.length} siblings agree on ${majorityKey}`,
|
|
64
|
+
fix: `Set '${checkConfig.field}' to ${majorityKey} to match its siblings (or justify the difference).`,
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const report = findings.length
|
|
69
|
+
? `# Sibling consensus — ${checkConfig.family} · ${checkConfig.field}\n\nMajority (${majorityCount}/${members.length}): ${majorityKey}\n\n${findings.map((f) => `- WARN ${f.file} — ${f.message}`).join("\n")}\n\nErrors: 0\nWarnings: ${findings.length}\n`
|
|
70
|
+
: "";
|
|
71
|
+
return { failed: false, findings, report };
|
|
72
|
+
},
|
|
73
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// CORE check — conformance oracle in REFERENCE mode applied to tsconfig.json compilerOptions.
|
|
2
|
+
// Walks all tsconfig.json files; the reference (default: root tsconfig.json) defines the expected values
|
|
3
|
+
// for any compilerOption key it declares. Any tsconfig whose values for those shared keys differ from the
|
|
4
|
+
// reference is flagged. Uses runFamilyConformance in reference mode — catches drift nobody has named.
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { relative, join, basename } from "node:path";
|
|
7
|
+
import { createFinding } from "../../core/finding.mjs";
|
|
8
|
+
import { walkFiles } from "../../core/fs-walk.mjs";
|
|
9
|
+
import { runFamilyConformance } from "../../core/oracle/family-conformance.mjs";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse a tsconfig.json — returns { compilerOptions: {...} } or null on failure.
|
|
13
|
+
*/
|
|
14
|
+
function parseTsconfig(file) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const audit = {
|
|
23
|
+
id: "tsconfig-conformance",
|
|
24
|
+
title: "tsconfig.json files conform to reference compilerOptions",
|
|
25
|
+
category: "conformance",
|
|
26
|
+
domain: "code",
|
|
27
|
+
requires: { ecosystems: ["npm"] },
|
|
28
|
+
gating: false,
|
|
29
|
+
defaultConfig: { referenceFile: "tsconfig.json" }, // relative to root
|
|
30
|
+
async run(ctx) {
|
|
31
|
+
const { root, checkConfig } = ctx;
|
|
32
|
+
const refPath = join(root, checkConfig.referenceFile);
|
|
33
|
+
|
|
34
|
+
// Parse the reference to extract its compilerOptions keys — these are the axes we judge.
|
|
35
|
+
const refParsed = parseTsconfig(refPath);
|
|
36
|
+
const refCompilerOptions = refParsed?.compilerOptions || {};
|
|
37
|
+
const refKeys = Object.keys(refCompilerOptions);
|
|
38
|
+
if (refKeys.length === 0) {
|
|
39
|
+
// No compilerOptions in reference — nothing to judge.
|
|
40
|
+
return { failed: false, findings: [], report: "" };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Collect all tsconfig.json files (including the reference — the oracle skips it).
|
|
44
|
+
const members = [];
|
|
45
|
+
for (const file of walkFiles(root)) {
|
|
46
|
+
if (basename(file) === "tsconfig.json") members.push({ path: file });
|
|
47
|
+
}
|
|
48
|
+
if (members.length === 0) return { failed: false, findings: [], report: "" };
|
|
49
|
+
|
|
50
|
+
// Fingerprint = the subset of compilerOptions present in the REFERENCE — so we only compare apples to apples.
|
|
51
|
+
const project = (member) => {
|
|
52
|
+
const parsed = parseTsconfig(member.path);
|
|
53
|
+
const co = parsed?.compilerOptions || {};
|
|
54
|
+
// Build a sub-object with ONLY the keys from the reference's compilerOptions.
|
|
55
|
+
const subset = {};
|
|
56
|
+
for (const k of refKeys) {
|
|
57
|
+
if (Object.prototype.hasOwnProperty.call(co, k)) {
|
|
58
|
+
subset[k] = co[k];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return subset;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Reference fingerprint is the subset of its OWN compilerOptions keys.
|
|
65
|
+
const { findings: divergences } = await runFamilyConformance({
|
|
66
|
+
family: members,
|
|
67
|
+
project,
|
|
68
|
+
mode: "reference",
|
|
69
|
+
referenceId: refPath,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!divergences || divergences.length === 0) {
|
|
73
|
+
return { failed: false, findings: [], report: "" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const findings = [];
|
|
77
|
+
for (const d of divergences) {
|
|
78
|
+
const relPath = relative(root, d.member.path);
|
|
79
|
+
// Enumerate only the keys that actually differ.
|
|
80
|
+
const expected = d.expected || {};
|
|
81
|
+
const actual = d.actual || {};
|
|
82
|
+
const allKeys = new Set([...Object.keys(expected), ...Object.keys(actual)]);
|
|
83
|
+
const diffKeys = [...allKeys].filter(
|
|
84
|
+
(k) => JSON.stringify(expected[k]) !== JSON.stringify(actual[k]),
|
|
85
|
+
);
|
|
86
|
+
for (const k of diffKeys) {
|
|
87
|
+
findings.push(
|
|
88
|
+
createFinding({
|
|
89
|
+
id: "tsconfig-drift",
|
|
90
|
+
title: `tsconfig compilerOption '${k}' diverges from reference`,
|
|
91
|
+
severity: "warning",
|
|
92
|
+
file: relPath,
|
|
93
|
+
message: `compilerOptions.${k}: expected ${JSON.stringify(expected[k])}, got ${JSON.stringify(actual[k])} (reference: ${checkConfig.referenceFile})`,
|
|
94
|
+
fix: `Set compilerOptions.${k} to ${JSON.stringify(expected[k])} to match ${checkConfig.referenceFile}, or remove it from the reference if it should not be enforced globally.`,
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const report = findings.length
|
|
101
|
+
? `# tsconfig conformance\n\nReference: ${checkConfig.referenceFile}\n\n${findings.map((f) => `- WARN ${f.file} — ${f.message}`).join("\n")}\n\nErrors: 0\nWarnings: ${findings.length}\n`
|
|
102
|
+
: "";
|
|
103
|
+
return { failed: false, findings, report };
|
|
104
|
+
},
|
|
105
|
+
};
|