connections-arkitect 0.3.4

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.
@@ -0,0 +1,237 @@
1
+ // HOSTED check — the generic justify-existence governor for a live Postgres DB (distilled from
2
+ // Connections' internal live-DB governor). Runs ONLY through the MCP vault (domain:"hosted" → skipped
3
+ // standalone). Judges every table on the 3-axis model (exact DATA rows × external APP-CODE refs ×
4
+ // internal DB refs) against the user's spine, and emits the 5 verdicts + a review-only remediation
5
+ // block. READ-ONLY — it never mutates the DB.
6
+ //
7
+ // Gating: `gating: true` + severity does the work. Default severities top out at "warning" (advisory
8
+ // governance), so the check never gates. With `hosted.failOnBounty: true` (or --fail-on-bounty),
9
+ // CONDEMNED / PROVE-OR-DIE escalate to gating errors — the CI / pre-deploy posture.
10
+ import { readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { execFileSync } from "node:child_process";
13
+ import { createFinding } from "../../core/finding.mjs";
14
+ import { walkFiles } from "../../core/fs-walk.mjs";
15
+ import { loadSpine, resolveClaim } from "../../core/hosted/spine.mjs";
16
+ import { judgeObject, verdictSeverity, VERDICT_RANK } from "../../core/hosted/judge.mjs";
17
+ import { vaultRdsQuery } from "../../core/hosted/vault-exec.mjs";
18
+
19
+ // Exact per-table row count (query_to_xml so one round trip covers every table) + physical size.
20
+ // n_live_tup is an ANALYZE-dependent ESTIMATE — a stale-stats table would read 0 rows, judge "inert",
21
+ // and earn a wrongful CONDEMNED; exactness is the whole point of the DATA axis.
22
+ const TABLES_SQL = `
23
+ SELECT c.relname AS name,
24
+ (xpath('/row/cnt/text()', query_to_xml(format('SELECT count(*) AS cnt FROM public.%I', c.relname), false, true, '')))[1]::text::bigint AS rows,
25
+ pg_total_relation_size(c.oid) AS bytes
26
+ FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
27
+ WHERE n.nspname = 'public' AND c.relkind = 'r' ORDER BY c.relname`;
28
+ const INTERNAL_SRC_SQL =
29
+ "select prosrc as src from pg_proc where pronamespace = 'public'::regnamespace " +
30
+ "union all select definition as src from pg_views where schemaname = 'public'";
31
+
32
+ const SUPPORTED_KINDS = new Set(["aurora-postgres-data-api"]);
33
+ const CODE_EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".rs", ".go", ".rb", ".java", ".php", ".sql"]);
34
+
35
+ export const audit = {
36
+ id: "db-justify-existence",
37
+ title: "Database — justify existence (hosted)",
38
+ category: "governance",
39
+ domain: "hosted",
40
+ outputContract: "parsed-findings",
41
+ requires: {},
42
+ gating: true,
43
+ defaultConfig: {},
44
+ async run(ctx) {
45
+ const { root, config, vault } = ctx;
46
+ const failOnBounty = config?.hosted?.failOnBounty === true || ctx.options?.failOnBounty === true;
47
+ const declared = config?.hosted?.targets || [];
48
+ const targets = declared.filter((t) => SUPPORTED_KINDS.has(t.kind));
49
+ const findings = [];
50
+
51
+ // A declared target the engine can't reach must be LOUD — silence here reads as "governed" when
52
+ // nothing was checked at all.
53
+ for (const t of declared.filter((t) => !SUPPORTED_KINDS.has(t.kind))) {
54
+ findings.push(
55
+ createFinding({
56
+ id: "db-unsupported-target",
57
+ title: `unsupported hosted target kind: ${t.kind ?? "(none)"}`,
58
+ severity: "warning",
59
+ resource: t.database || t.label || "(unnamed target)",
60
+ message: `hosted.targets entry has kind "${t.kind ?? "(none)"}" — supported: ${[...SUPPORTED_KINDS].join(", ")}. This target was NOT checked.`,
61
+ }),
62
+ );
63
+ }
64
+ if (!targets.length) return { failed: false, findings, report: findings.length ? renderReport(findings, [], 0) : "" };
65
+
66
+ const killOrders = [];
67
+ const refCounters = new Map(); // appCodeRoots-key → counter fn (memoized per distinct root set)
68
+ let bountyErrors = 0;
69
+
70
+ for (const target of targets) {
71
+ // The user's spine (the policy — their `your-checks/` for the hosted half). Per-target
72
+ // `target.spine` wins over the shared `hosted.spine`; paths are repo-relative.
73
+ const spinePath = target.spine || config?.hosted?.spine || null;
74
+ let spine = loadSpine({});
75
+ if (spinePath) {
76
+ try {
77
+ spine = loadSpine(JSON.parse(readFileSync(join(root, spinePath), "utf8")));
78
+ } catch (e) {
79
+ // A broken spine silently judging everything PROVE-OR-DIE is a policy outage — say so.
80
+ findings.push(
81
+ createFinding({
82
+ id: "db-spine-unreadable",
83
+ title: `spine unreadable: ${spinePath}`,
84
+ severity: "warning",
85
+ resource: target.database || spinePath,
86
+ message: `Could not read/parse spine "${spinePath}" (${String(e?.message || e).slice(0, 120)}). Every object in ${target.database} is being judged UNRECORDED.`,
87
+ }),
88
+ );
89
+ }
90
+ }
91
+ const rootsKey = JSON.stringify(spine.appCodeRoots || []);
92
+ if (!refCounters.has(rootsKey)) refCounters.set(rootsKey, makeAppRefCounter(root, spine.appCodeRoots, findings));
93
+ const appRefs = refCounters.get(rootsKey);
94
+
95
+ const tables = await vaultRdsQuery(vault, target, TABLES_SQL);
96
+ if (!tables.length) {
97
+ // Refuse to emit an all-clear from an empty read — 0 tables means the introspection failed or
98
+ // pointed at the wrong DB far more often than it means a genuinely empty schema.
99
+ const sev = failOnBounty ? "error" : "warning";
100
+ if (sev === "error") bountyErrors++;
101
+ findings.push(
102
+ createFinding({
103
+ id: "db-empty-introspection",
104
+ title: `introspection returned 0 tables: ${target.database}`,
105
+ severity: sev,
106
+ resource: `${target.database}`,
107
+ account: target.vaultInstance,
108
+ message: `Introspection of ${target.database} returned 0 tables — refusing to treat an empty read as an all-clear. Verify the target coordinates.`,
109
+ }),
110
+ );
111
+ continue;
112
+ }
113
+ const internalSrc = (await vaultRdsQuery(vault, target, INTERNAL_SRC_SQL)).map((r) => String(r.src ?? "")).join("\n");
114
+
115
+ for (const t of tables) {
116
+ const name = String(t.name);
117
+ const rows = Number(t.rows) || 0;
118
+ const dbRefs = countOccurrences(internalSrc, name);
119
+ const codeRefs = appRefs(name);
120
+ const alive = rows > 0 || dbRefs > 0 || codeRefs > 0;
121
+ const claim = resolveClaim(spine, name);
122
+ const verdict = judgeObject({
123
+ declared: claim.declared,
124
+ alive,
125
+ investigate: claim.investigate,
126
+ claimedRemove: claim.claimedRemove,
127
+ });
128
+ const sev = verdictSeverity(verdict, { failOnBounty });
129
+ if (!sev) continue; // JUSTIFIED
130
+ if (sev === "error") bountyErrors++;
131
+ findings.push(
132
+ createFinding({
133
+ id: "db-object-verdict",
134
+ title: `${verdict}: ${name}`,
135
+ severity: sev,
136
+ resource: `${target.database}.${name}`,
137
+ account: target.vaultInstance,
138
+ verdict,
139
+ message:
140
+ `${verdict} — rows=${rows}, app-refs=${codeRefs}, db-refs=${dbRefs}, declared=${claim.declared}` +
141
+ `${claim.claimedBy ? ` (by ${claim.claimedBy})` : ""}${claim.reason ? ` — ${claim.reason}` : ""}`,
142
+ fix: verdict === "PROVE-OR-DIE" ? `Add a spine claim for "${name}" or drop it.` : null,
143
+ }),
144
+ );
145
+ if (verdict === "CONDEMNED") {
146
+ killOrders.push(
147
+ `-- ${target.database}.${name}: CONDEMNED (rows=${rows}, app-refs=${codeRefs}, db-refs=${dbRefs})\n-- drop table if exists "${name}"; -- REVIEW + previewed read before running`,
148
+ );
149
+ }
150
+ }
151
+ }
152
+
153
+ findings.sort((a, b) => (VERDICT_RANK[a.verdict] ?? 9) - (VERDICT_RANK[b.verdict] ?? 9));
154
+ const warnings = findings.filter((f) => f.severity === "warning").length;
155
+ const report = findings.length ? renderReport(findings, killOrders, warnings) : "";
156
+ return { failed: bountyErrors > 0, findings, report };
157
+ },
158
+ };
159
+
160
+ function renderReport(findings, killOrders, warnings) {
161
+ return (
162
+ `# DB justify-existence\n\n${findings.map((f) => `- ${f.verdict ?? f.severity.toUpperCase()} ${f.resource} — ${f.message}`).join("\n")}` +
163
+ (killOrders.length
164
+ ? `\n\n## Remediation — REVIEW + previewed read before running (never auto-executed)\n\n\`\`\`sql\n${killOrders.join("\n\n")}\n\`\`\``
165
+ : "") +
166
+ `\n\nErrors: ${findings.filter((f) => f.severity === "error").length}\nWarnings: ${warnings}\n`
167
+ );
168
+ }
169
+
170
+ // APP-CODE axis — count external references to a table name. Preferred transport: `git grep -w`
171
+ // against the target repo (exact, unbounded, fast — the whole tracked tree, or the spine's
172
+ // appCodeRoots as pathspecs). Fallback for non-git roots: an in-memory blob, bounded — and when the
173
+ // bound truncates, it SAYS so (a silently-truncated axis reads as "unreferenced" and feeds wrongful
174
+ // CONDEMNED verdicts).
175
+ function makeAppRefCounter(root, appCodeRoots, findings) {
176
+ try {
177
+ execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root, stdio: "pipe" });
178
+ const pathspecs = appCodeRoots && appCodeRoots.length ? appCodeRoots : [];
179
+ return (name) => {
180
+ try {
181
+ const out = execFileSync("git", ["grep", "-lw", "-e", name, "--", ...pathspecs], {
182
+ cwd: root,
183
+ encoding: "utf8",
184
+ maxBuffer: 64 * 1024 * 1024,
185
+ });
186
+ return out.split("\n").filter(Boolean).length;
187
+ } catch {
188
+ return 0; // git grep exits non-zero on zero matches
189
+ }
190
+ };
191
+ } catch {
192
+ /* not a git work tree — fall back to the bounded blob */
193
+ }
194
+ const { blob, truncated } = readCodeBlob(root, appCodeRoots);
195
+ if (truncated) {
196
+ findings.push(
197
+ createFinding({
198
+ id: "db-app-axis-truncated",
199
+ title: "app-code reference axis truncated",
200
+ severity: "warning",
201
+ resource: root,
202
+ message:
203
+ "Non-git root: the app-code scan hit its file budget before covering the repo — app-refs counts below may UNDERCOUNT. Set spine.appCodeRoots to the real code roots, or run inside a git work tree.",
204
+ }),
205
+ );
206
+ }
207
+ return (name) => countOccurrences(blob, name);
208
+ }
209
+
210
+ function readCodeBlob(root, appCodeRoots) {
211
+ const roots = appCodeRoots && appCodeRoots.length ? appCodeRoots.map((r) => join(root, r)) : [root];
212
+ let blob = "";
213
+ let budget = 4000;
214
+ let truncated = false;
215
+ for (const r of roots) {
216
+ for (const file of walkFiles(r)) {
217
+ if (!CODE_EXT.has(file.slice(file.lastIndexOf(".")))) continue;
218
+ if (budget-- <= 0) {
219
+ truncated = true;
220
+ break;
221
+ }
222
+ try {
223
+ blob += "\n" + readFileSync(file, "utf8");
224
+ } catch {
225
+ /* skip unreadable */
226
+ }
227
+ }
228
+ }
229
+ return { blob, truncated };
230
+ }
231
+
232
+ function countOccurrences(haystack, name) {
233
+ if (!name) return 0;
234
+ const re = new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "g");
235
+ const m = haystack.match(re);
236
+ return m ? m.length : 0;
237
+ }
@@ -0,0 +1,76 @@
1
+ // HOSTED check — required database ROLES exist. Generic breadth-class governance: an application
2
+ // engine that runs `SET LOCAL ROLE <x>` (or grants through a role) on every call hard-fails EVERY
3
+ // request the moment a database is stood up without that role — an outage class that only shows up
4
+ // at runtime, on the newest plane, at the worst time. Declare the roles a target's engine depends on
5
+ // and this check asserts they exist, per target:
6
+ //
7
+ // "hosted": { "targets": [{ ..., "requiredRoles": ["app_user_role"] }] }
8
+ //
9
+ // No requiredRoles declared ⇒ silently inapplicable. READ-ONLY. Missing roles are definitively-broken
10
+ // states: warning by default, gating errors under hosted.failOnBounty (the CI / pre-deploy posture).
11
+ import { createFinding } from "../../core/finding.mjs";
12
+ import { vaultRdsQuery } from "../../core/hosted/vault-exec.mjs";
13
+
14
+ export const audit = {
15
+ id: "db-required-roles",
16
+ title: "Database — required roles exist (hosted)",
17
+ category: "governance",
18
+ domain: "hosted",
19
+ requires: {},
20
+ gating: true,
21
+ defaultConfig: {},
22
+ async run(ctx) {
23
+ const { config, vault } = ctx;
24
+ const failOnBounty = config?.hosted?.failOnBounty === true || ctx.options?.failOnBounty === true;
25
+ const targets = (config?.hosted?.targets || []).filter(
26
+ (t) => t.kind === "aurora-postgres-data-api" && Array.isArray(t.requiredRoles) && t.requiredRoles.length,
27
+ );
28
+ const findings = [];
29
+ let errors = 0;
30
+
31
+ for (const target of targets) {
32
+ const wanted = target.requiredRoles.map(String);
33
+ const inList = wanted.map((r) => `'${r.replace(/'/g, "''")}'`).join(", ");
34
+ let present;
35
+ try {
36
+ present = (await vaultRdsQuery(vault, target, `SELECT rolname FROM pg_roles WHERE rolname IN (${inList})`)).map((r) =>
37
+ String(r.rolname),
38
+ );
39
+ } catch (e) {
40
+ const sev = failOnBounty ? "error" : "warning";
41
+ if (sev === "error") errors++;
42
+ findings.push(
43
+ createFinding({
44
+ id: "db-roles-probe-failed",
45
+ title: `role probe failed: ${target.database}`,
46
+ severity: sev,
47
+ resource: target.database,
48
+ account: target.vaultInstance,
49
+ message: `Could not read pg_roles on ${target.database} (${String(e?.message || e).slice(0, 140)}) — required roles UNVERIFIED, not a pass.`,
50
+ }),
51
+ );
52
+ continue;
53
+ }
54
+ for (const role of wanted.filter((r) => !present.includes(r))) {
55
+ const sev = failOnBounty ? "error" : "warning";
56
+ if (sev === "error") errors++;
57
+ findings.push(
58
+ createFinding({
59
+ id: "db-required-role-missing",
60
+ title: `missing role: ${role} on ${target.database}`,
61
+ severity: sev,
62
+ resource: `${target.database} role ${role}`,
63
+ account: target.vaultInstance,
64
+ message: `Role "${role}" does not exist on ${target.database} — an engine that SETs/GRANTs through it fails EVERY call on this DB. Create the role before traffic does this for you.`,
65
+ fix: `CREATE ROLE "${role}"; -- plus the grants your engine's migration normally applies`,
66
+ }),
67
+ );
68
+ }
69
+ }
70
+
71
+ const report = findings.length
72
+ ? `# DB required roles\n\n${findings.map((f) => `- [${f.severity.toUpperCase()}] ${f.resource} — ${f.message}`).join("\n")}\n`
73
+ : "";
74
+ return { failed: errors > 0, findings, report };
75
+ },
76
+ };
@@ -0,0 +1,50 @@
1
+ // Workspace config loading — ONE implementation for every door (CLI + the MCP runner).
2
+ //
3
+ // Lookup order (first hit wins): <root>/.arkitect/arkitect.config.json (the tucked-away
4
+ // per-workspace placement) then <root>/arkitect.config.json.
5
+ //
6
+ // `extends` (tsconfig/eslint-style): a config may list other config FILES (repo-root-relative) whose
7
+ // contents it inherits — objects deep-merge with the extending file winning, arrays and scalars
8
+ // replace. Lets a workspace layer its Architect config over an existing tool's config file instead of
9
+ // duplicating it (the convergence case: inheriting a legacy engine's paths/checks config verbatim).
10
+ // Chains resolve recursively; cycles are ignored rather than fatal.
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+
14
+ function deepMerge(base, over) {
15
+ if (Array.isArray(base) || Array.isArray(over) || typeof base !== "object" || typeof over !== "object" || !base || !over) {
16
+ return over === undefined ? base : over;
17
+ }
18
+ const out = { ...base };
19
+ for (const [k, v] of Object.entries(over)) out[k] = deepMerge(base[k], v);
20
+ return out;
21
+ }
22
+
23
+ function loadFile(root, rel, seen, errors) {
24
+ const abs = join(root, rel);
25
+ if (seen.has(abs)) return {}; // cycle — inherit nothing twice
26
+ seen.add(abs);
27
+ let raw;
28
+ try {
29
+ raw = JSON.parse(readFileSync(abs, "utf8"));
30
+ } catch (e) {
31
+ errors.push({ file: rel, error: String(e?.message || e) });
32
+ return {};
33
+ }
34
+ const bases = Array.isArray(raw.extends) ? raw.extends : raw.extends ? [raw.extends] : [];
35
+ let merged = {};
36
+ for (const b of bases) merged = deepMerge(merged, loadFile(root, b, seen, errors));
37
+ const { extends: _drop, ...own } = raw;
38
+ return deepMerge(merged, own);
39
+ }
40
+
41
+ /**
42
+ * Load the workspace's Architect config (with `extends` resolution).
43
+ * @returns {{ config: object, path: string|null, errors: Array<{file, error}> }}
44
+ */
45
+ export function loadWorkspaceConfig(root) {
46
+ const errors = [];
47
+ const path = [join(".arkitect", "arkitect.config.json"), "arkitect.config.json"].find((p) => existsSync(join(root, p))) ?? null;
48
+ if (!path) return { config: {}, path: null, errors };
49
+ return { config: loadFile(root, path, new Set(), errors), path, errors };
50
+ }
@@ -0,0 +1,41 @@
1
+ // Auto-discovery — the heart of the core/extension model. A check is ANY .mjs that exports `audit`
2
+ // with an `id` + `run()`. No registry to edit; the file IS the registration. We scan MULTIPLE roots in
3
+ // priority order and merge: later roots OVERRIDE earlier by id, so a user's `your-checks/` check shadows
4
+ // a core check of the same id (patch a core check without touching the core). The folder name = the group.
5
+ import { existsSync } from "node:fs";
6
+ import { basename, dirname } from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import { walkFiles } from "./fs-walk.mjs";
9
+
10
+ export async function discoverAudits(roots) {
11
+ const byId = new Map();
12
+ const loadErrors = [];
13
+ const shadowed = [];
14
+ for (const root of roots) {
15
+ if (!root || !existsSync(root)) continue;
16
+ for (const file of walkFiles(root)) {
17
+ if (!file.endsWith(".mjs") || file.endsWith(".test.mjs")) continue;
18
+ let mod;
19
+ try {
20
+ mod = await import(pathToFileURL(file).href);
21
+ } catch (e) {
22
+ // A check that won't even load must be LOUD (crash≠silent-pass), never silently dropped.
23
+ loadErrors.push({ file, error: String(e?.message || e) });
24
+ continue;
25
+ }
26
+ // The canonical export is `audit`, but ANY audit-shaped export registers ({ id, run }) — and a
27
+ // file may export several (e.g. a defineX() factory producing a family). Zero wiring either way.
28
+ const candidates = [mod.audit, ...Object.values(mod).filter((v) => v !== mod.audit)];
29
+ for (const audit of candidates) {
30
+ if (!audit || typeof audit !== "object" || typeof audit.id !== "string" || typeof audit.run !== "function") continue;
31
+ if (byId.get(audit.id) === audit) continue; // same object exported under two names
32
+ if (byId.has(audit.id)) shadowed.push({ id: audit.id, by: file });
33
+ audit.__file = file;
34
+ audit.__group = basename(dirname(file));
35
+ audit.__root = root;
36
+ byId.set(audit.id, audit);
37
+ }
38
+ }
39
+ }
40
+ return { audits: [...byId.values()], loadErrors, shadowed };
41
+ }
@@ -0,0 +1,40 @@
1
+ // The one Finding shape every check emits — for code AND hosted/infra checks. Distilled from arkitect's
2
+ // normalized finding + risk scoring. A check may also return plain objects of this shape; createFinding
3
+ // just fills defaults. Carries infra fields (resource/account/region) so the hosted half reuses it verbatim.
4
+
5
+ export const SEVERITY = Object.freeze({ error: "error", warning: "warning", info: "info" });
6
+
7
+ export function createFinding({
8
+ id,
9
+ title,
10
+ severity = "error",
11
+ file = null,
12
+ line = null,
13
+ message = "",
14
+ resource = null, // hosted: an ARN / table / bucket / role
15
+ account = null, // hosted: which cloud account
16
+ region = null,
17
+ verdict = null, // hosted: JUSTIFIED | WATCH | PROVE-OR-DIE | INVESTIGATE | CONDEMNED
18
+ fix = null, // a one-line suggested remediation
19
+ } = {}) {
20
+ return { id, title, severity, file, line, message: message || title, resource, account, region, verdict, fix };
21
+ }
22
+
23
+ export function countSeverities(findings = []) {
24
+ let errors = 0,
25
+ warnings = 0,
26
+ infos = 0;
27
+ for (const f of findings) {
28
+ if (f.severity === "error") errors++;
29
+ else if (f.severity === "warning") warnings++;
30
+ else infos++;
31
+ }
32
+ return { errors, warnings, infos };
33
+ }
34
+
35
+ // Cheap risk ordering for the FIX_QUEUE — errors first, then by reachability hints. Kept deliberately
36
+ // simple in Phase 1; the full arkitect risk axes (exposure × data × reachability × safety-net) land in Phase 2.
37
+ export function riskRank(finding) {
38
+ const sev = finding.severity === "error" ? 0 : finding.severity === "warning" ? 1 : 2;
39
+ return sev;
40
+ }
@@ -0,0 +1,24 @@
1
+ // One filesystem walker, reused by discovery + every check (DRY). Skips the usual build/vendor dirs.
2
+ import { readdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { IGNORE_DIRS, isIgnoredDir } from "./project-detect.mjs";
5
+
6
+ export function* walkFiles(dir, { ignore = IGNORE_DIRS, includeDotfiles = false } = {}) {
7
+ let entries;
8
+ try {
9
+ entries = readdirSync(dir, { withFileTypes: true });
10
+ } catch {
11
+ return;
12
+ }
13
+ for (const e of entries) {
14
+ if (!includeDotfiles && e.name.startsWith(".")) continue;
15
+ const p = join(dir, e.name);
16
+ if (e.isDirectory()) {
17
+ // isIgnoredDir is the canonical generated-output matcher (names + dist-* family + Capacitor
18
+ // webDir copies); a custom `ignore` set ADDS to it, never re-admits build output.
19
+ if (!isIgnoredDir(e.name, p) && !ignore.has(e.name)) yield* walkFiles(p, { ignore, includeDotfiles });
20
+ } else {
21
+ yield p;
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,42 @@
1
+ // The generic "justify existence" judge — the 3-axis verdict system distilled from Connections' internal
2
+ // live-DB governor, generalized so it works on ANY live resource (a DB table, an S3 bucket, an IAM role),
3
+ // not just one schema.
4
+ //
5
+ // The model: judge each live object on DATA (rows) × APP-CODE refs × internal refs, against a
6
+ // per-workspace "justify existence" spine. An object is JUSTIFIED only when it is BOTH
7
+ // • DECLARED — a human wrote down why it exists (a spine claim), AND
8
+ // • ALIVE — something actually uses it (holds data, or is referenced by external code, or by another
9
+ // internal object).
10
+ // An explicit spine verdict (INVESTIGATE / REMOVE) overrides the inference — so a squatter can't be
11
+ // auto-justified by a loose prefix-claim, and a human kill-order is honored.
12
+
13
+ // Ranked worst→best, for sorting kill-orders.
14
+ export const VERDICT_RANK = Object.freeze({ CONDEMNED: 0, "PROVE-OR-DIE": 1, INVESTIGATE: 2, WATCH: 3, JUSTIFIED: 4 });
15
+
16
+ /**
17
+ * Judge ONE object.
18
+ * @param {object} o
19
+ * @param {boolean} o.declared - the spine claims this object (a recorded reason to exist)
20
+ * @param {boolean} o.alive - anything references/uses it (rows > 0 OR external ref OR internal ref)
21
+ * @param {boolean} [o.investigate] - the spine explicitly flags it for human review (overrides)
22
+ * @param {boolean} [o.claimedRemove] - the spine explicitly orders removal (overrides)
23
+ * @returns {"JUSTIFIED"|"WATCH"|"PROVE-OR-DIE"|"INVESTIGATE"|"CONDEMNED"}
24
+ */
25
+ export function judgeObject({ declared, alive, investigate = false, claimedRemove = false }) {
26
+ if (investigate) return "INVESTIGATE"; // explicit human flag wins
27
+ if (claimedRemove) return "CONDEMNED"; // explicit kill-order wins
28
+ if (declared && alive) return "JUSTIFIED"; // recorded AND used
29
+ if (declared && !alive) return "WATCH"; // recorded but inert — keep an eye on it
30
+ if (!declared && alive) return "PROVE-OR-DIE"; // used but no recorded reason — earn a spine entry or die
31
+ return "CONDEMNED"; // unrecorded AND inert
32
+ }
33
+
34
+ // Verdict → finding severity. This is a read-only GOVERNANCE judgment (recommends, never mutates), so by
35
+ // default the strongest verdicts surface as warnings, the softer ones as info. JUSTIFIED produces no
36
+ // finding. Bounty mode (`hosted.failOnBounty: true` / --fail-on-bounty) escalates CONDEMNED and
37
+ // PROVE-OR-DIE to gating errors so the governor can gate a CI run or a DB-touching deploy.
38
+ export function verdictSeverity(verdict, { failOnBounty = false } = {}) {
39
+ if (verdict === "CONDEMNED" || verdict === "PROVE-OR-DIE") return failOnBounty ? "error" : "warning";
40
+ if (verdict === "WATCH" || verdict === "INVESTIGATE") return "info";
41
+ return null; // JUSTIFIED
42
+ }
@@ -0,0 +1,63 @@
1
+ // The "justify existence" SPINE — the user-extension space for the HOSTED half. The user declares WHY
2
+ // each live object exists; the framework judges against it.
3
+ // This is the hosted equivalent of `your-checks/`: the policy is the user's, the engine is the core's.
4
+ //
5
+ // Shape (justify-existence.json):
6
+ // {
7
+ // "appCodeRoots": ["src", "services"], // where to grep for external references (default: repo root)
8
+ // "features": [ // each feature CLAIMS the objects it owns
9
+ // { "name": "billing", "claims": ["invoices", "payment_*"] }
10
+ // ],
11
+ // "verdicts": { "legacy_temp": "REMOVE", "weird_table": "INVESTIGATE" } // explicit overrides
12
+ // }
13
+ // A claim ending in `*` is a prefix claim; otherwise it's an exact name.
14
+ //
15
+ // A second, equivalent dialect is accepted so existing per-plane spines drop in unconverted:
16
+ // features: { "id": "billing", "tables": ["invoices"], "tablePrefixes": ["payment"] }
17
+ // verdicts: { "legacy_temp": { "verdict": "REMOVE", "reason": "superseded by X" } }
18
+ // `tables` are exact claims, each `tablePrefixes` entry `p` becomes the prefix claim `p*`, and a verdict
19
+ // object's `reason` is carried through to the finding message.
20
+
21
+ export function loadSpine(json) {
22
+ const s = json && typeof json === "object" ? json : {};
23
+ const features = (Array.isArray(s.features) ? s.features : []).map((f) => ({
24
+ name: f?.name || f?.id || null,
25
+ claims: [
26
+ ...(Array.isArray(f?.claims) ? f.claims : []),
27
+ ...(Array.isArray(f?.tables) ? f.tables : []),
28
+ ...(Array.isArray(f?.tablePrefixes) ? f.tablePrefixes.map((p) => `${p}*`) : []),
29
+ ].filter((c) => typeof c === "string" && c.length),
30
+ }));
31
+ // Normalize verdict values: "REMOVE" | { verdict: "REMOVE", reason?: "…" } → { verdict, reason }.
32
+ const verdicts = {};
33
+ if (s.verdicts && typeof s.verdicts === "object") {
34
+ for (const [name, v] of Object.entries(s.verdicts)) {
35
+ if (typeof v === "string") verdicts[name] = { verdict: v.toUpperCase(), reason: null };
36
+ else if (v && typeof v === "object" && typeof v.verdict === "string")
37
+ verdicts[name] = { verdict: v.verdict.toUpperCase(), reason: v.reason ?? null };
38
+ }
39
+ }
40
+ return { appCodeRoots: Array.isArray(s.appCodeRoots) && s.appCodeRoots.length ? s.appCodeRoots : null, features, verdicts };
41
+ }
42
+
43
+ /**
44
+ * Resolve what the spine says about an object by name.
45
+ * Explicit `verdicts[name]` overrides any feature claim (so a prefix-claim can't auto-justify a squatter,
46
+ * and a human REMOVE/INVESTIGATE is honored).
47
+ * @returns {{ declared: boolean, investigate: boolean, claimedRemove: boolean, claimedBy: string|null, reason: string|null }}
48
+ */
49
+ export function resolveClaim(spine, name) {
50
+ const override = spine.verdicts?.[name];
51
+ if (override?.verdict === "INVESTIGATE")
52
+ return { declared: true, investigate: true, claimedRemove: false, claimedBy: "verdicts", reason: override.reason ?? null };
53
+ if (override?.verdict === "REMOVE")
54
+ return { declared: true, investigate: false, claimedRemove: true, claimedBy: "verdicts", reason: override.reason ?? null };
55
+
56
+ for (const f of spine.features || []) {
57
+ for (const claim of f.claims || []) {
58
+ const matched = claim.endsWith("*") ? name.startsWith(claim.slice(0, -1)) : claim === name;
59
+ if (matched) return { declared: true, investigate: false, claimedRemove: false, claimedBy: f.name || claim, reason: null };
60
+ }
61
+ }
62
+ return { declared: false, investigate: false, claimedRemove: false, claimedBy: null, reason: null };
63
+ }
@@ -0,0 +1,61 @@
1
+ // Run a READ-ONLY query against the user's OWN cloud DB THROUGH the MCP vault — value-blind: the
2
+ // credential never enters the model context; the MCP injects it server-side (the same seam deploy_static
3
+ // uses). The `vault` handle is provided by the MCP-hosted runner. Standalone (`npx architect`, no MCP)
4
+ // there is no vault, so hosted checks are skipped (see runner.mjs) — the code half still runs.
5
+ //
6
+ // target: { kind:"aurora-postgres-data-api", vaultInstance, resourceArn, secretArn, database, region? }
7
+
8
+ export async function vaultRdsQuery(vault, target, sql) {
9
+ if (!vault || typeof vault.awsCall !== "function") {
10
+ throw new Error("hosted checks require the Connections MCP vault (no vault handle present)");
11
+ }
12
+ const res = await vault.awsCall({
13
+ instance: target.vaultInstance,
14
+ service: "rds-data",
15
+ region: target.region || "us-east-1",
16
+ method: "POST",
17
+ path: "/Execute",
18
+ body: JSON.stringify({
19
+ resourceArn: target.resourceArn,
20
+ secretArn: target.secretArn,
21
+ database: target.database,
22
+ sql,
23
+ includeResultMetadata: true,
24
+ }),
25
+ });
26
+ return parseRdsRecords(res);
27
+ }
28
+
29
+ // RDS Data API returns { records: [[{stringValue|longValue|…}, …], …], columnMetadata: [{name},…] }.
30
+ // Flatten to plain {col: value} rows. Pure → unit-testable.
31
+ export function parseRdsRecords(res) {
32
+ const body = typeof res?.body === "string" ? safeJson(res.body) : (res?.body ?? res);
33
+ if (!body || !Array.isArray(body.records)) return [];
34
+ const cols = (body.columnMetadata || []).map((c) => c.name);
35
+ return body.records.map((rec) => {
36
+ const row = {};
37
+ rec.forEach((cell, i) => {
38
+ row[cols[i] ?? i] = cellValue(cell);
39
+ });
40
+ return row;
41
+ });
42
+ }
43
+
44
+ function cellValue(cell) {
45
+ if (!cell || typeof cell !== "object") return cell;
46
+ if (cell.isNull) return null;
47
+ if ("stringValue" in cell) return cell.stringValue;
48
+ if ("longValue" in cell) return cell.longValue;
49
+ if ("doubleValue" in cell) return cell.doubleValue;
50
+ if ("booleanValue" in cell) return cell.booleanValue;
51
+ if ("blobValue" in cell) return cell.blobValue;
52
+ return null;
53
+ }
54
+
55
+ function safeJson(s) {
56
+ try {
57
+ return JSON.parse(s);
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
@@ -0,0 +1,15 @@
1
+ // Public API for check authors: `import { createFinding } from "connections-arkitect"`.
2
+ export { createFinding, countSeverities, riskRank, SEVERITY } from "./finding.mjs";
3
+ export { detectProject, checkAppliesToProject, IGNORE_DIRS } from "./project-detect.mjs";
4
+ export { walkFiles } from "./fs-walk.mjs";
5
+ export { discoverAudits } from "./discovery.mjs";
6
+ export { runAudits } from "./runner.mjs";
7
+ export { toSarif } from "./sarif.mjs";
8
+ export { runFamilyConformance } from "./oracle/family-conformance.mjs";
9
+ export { judgeObject, verdictSeverity, VERDICT_RANK } from "./hosted/judge.mjs";
10
+ export { loadSpine, resolveClaim } from "./hosted/spine.mjs";
11
+ export { vaultRdsQuery, parseRdsRecords } from "./hosted/vault-exec.mjs";
12
+ export { runSurfaceSizeCoverageAudit, SURFACE_SIZE_COVERAGE_DEFAULTS } from "../engines/surface/surface-size-coverage-engine.mjs";
13
+ export { selfUpdate, applyCoreManifest, isNewer, sha256Hex } from "../update/self-update.mjs";
14
+ export { initWorkspace } from "./init.mjs";
15
+ export { loadWorkspaceConfig } from "./config.mjs";