@biffo/cli 0.305.1 → 0.307.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/_skeletons/plugin-template/README.md +40 -0
- package/dist/index.js +369 -78
- package/package.json +1 -1
- package/scripts/verify.sh +1 -0
|
@@ -127,6 +127,46 @@ runtime config rather than tenant-scoped table rows:
|
|
|
127
127
|
> [ADR-0011](https://github.com/keiranholloway/biffo-template/blob/main/docs/ADR/0011-authorization-is-a-core-concern.md)
|
|
128
128
|
> (authorization is a Core concern, not a plugin) and no longer exists.
|
|
129
129
|
|
|
130
|
+
## User-facing frontend: the `user_frontend` contract (ADR-0021 §2)
|
|
131
|
+
|
|
132
|
+
If your plugin ships a founder-facing UI, it declares a `user_frontend` block
|
|
133
|
+
in `biffo.plugin.json`:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
"user_frontend": {
|
|
137
|
+
"dir": "web/dist",
|
|
138
|
+
"required_group": "founder"
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
and ships **`web/` and nothing else** — a built static bundle (`index.html`
|
|
143
|
+
plus hashed `assets/*`, the ordinary Vite build-output shape) at the
|
|
144
|
+
repo-relative path `dir` names. That is the entire contract. In particular a
|
|
145
|
+
`user_frontend` plugin ships **no** `terraform/` frontend resources and
|
|
146
|
+
**no** `deploy-frontend.yml` workflow of its own.
|
|
147
|
+
|
|
148
|
+
**Who serves it, and why there is nothing to provision.** The shared plugin
|
|
149
|
+
host serves the bundle directly — `GET /api/v1/plugins/<name>/ui` and
|
|
150
|
+
`GET /api/v1/plugins/<name>/ui/{proxy+}` on the API Gateway routes every
|
|
151
|
+
installed plugin already shares — the same way it already serves
|
|
152
|
+
`admin_ingress`'s admin shell. There is no per-plugin S3 origin, no per-plugin
|
|
153
|
+
CloudFront behaviour, and nothing for `biffo plugin install` to provision:
|
|
154
|
+
committing `web/dist/` is the whole deploy. See
|
|
155
|
+
[ADR-0021](../../docs/ADR/0021-shared-plugin-hosting.md) section 2 for the
|
|
156
|
+
full design and why this superseded the older, per-plugin hosting model.
|
|
157
|
+
|
|
158
|
+
**This retires ADR-0018 section 2's per-plugin frontend shape** — a dedicated
|
|
159
|
+
`aws_s3_bucket` behind the shared CloudFront distribution, wired through a
|
|
160
|
+
`cdn_distribution_arn` module variable and a `frontend_bucket_*` output. That
|
|
161
|
+
shape is fail-closed at install time: `biffo plugin install` and
|
|
162
|
+
`biffo plugin upgrade` **refuse** a plugin whose `terraform/` declares it —
|
|
163
|
+
an `aws_s3_bucket` whose name interpolates `-plugin-…-web`, an output named
|
|
164
|
+
`frontend_bucket_*`, or a `cdn_distribution_arn` variable — with an error
|
|
165
|
+
naming ADR-0021 section 2 and this contract. If you are updating an older
|
|
166
|
+
plugin that still ships one of these, delete `terraform/`'s frontend
|
|
167
|
+
resources (and `deploy-frontend.yml`, if present) and adopt `user_frontend`
|
|
168
|
+
above instead; there is nothing left to provision once you do.
|
|
169
|
+
|
|
130
170
|
## Standalone repo, not a monorepo package
|
|
131
171
|
|
|
132
172
|
Unlike biffo-template's own in-monorepo plugins (e.g. `services/_plugins/orchestrator/`,
|
package/dist/index.js
CHANGED
|
@@ -3504,6 +3504,45 @@ function declaredOutputs(moduleDir) {
|
|
|
3504
3504
|
}
|
|
3505
3505
|
return names;
|
|
3506
3506
|
}
|
|
3507
|
+
var RETIRED_FRONTEND_BUCKET_PATTERN = /bucket\s*=\s*"[^"]*-plugin-[^"]*-web"/;
|
|
3508
|
+
function declaresRetiredFrontendBucket(moduleDir) {
|
|
3509
|
+
let entries;
|
|
3510
|
+
try {
|
|
3511
|
+
entries = readdirSync3(moduleDir, { withFileTypes: true });
|
|
3512
|
+
} catch {
|
|
3513
|
+
return false;
|
|
3514
|
+
}
|
|
3515
|
+
for (const entry of entries) {
|
|
3516
|
+
if (!entry.isFile() || !entry.name.endsWith(".tf")) continue;
|
|
3517
|
+
let contents;
|
|
3518
|
+
try {
|
|
3519
|
+
contents = readFileSync10(join13(moduleDir, entry.name), "utf8");
|
|
3520
|
+
} catch {
|
|
3521
|
+
continue;
|
|
3522
|
+
}
|
|
3523
|
+
if (RETIRED_FRONTEND_BUCKET_PATTERN.test(contents)) return true;
|
|
3524
|
+
}
|
|
3525
|
+
return false;
|
|
3526
|
+
}
|
|
3527
|
+
function findRetiredFrontendShape(terraformDir) {
|
|
3528
|
+
const reasons = [];
|
|
3529
|
+
if (!existsSync12(terraformDir)) return reasons;
|
|
3530
|
+
if (declaresRetiredFrontendBucket(terraformDir)) {
|
|
3531
|
+
reasons.push('an aws_s3_bucket whose name interpolates "-plugin-...-web"');
|
|
3532
|
+
}
|
|
3533
|
+
for (const name of [...declaredOutputs(terraformDir)].sort()) {
|
|
3534
|
+
if (name.startsWith("frontend_bucket")) {
|
|
3535
|
+
reasons.push(`an output named "${name}"`);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
if (declaredVariables(terraformDir).has("cdn_distribution_arn")) {
|
|
3539
|
+
reasons.push('a "cdn_distribution_arn" variable');
|
|
3540
|
+
}
|
|
3541
|
+
return reasons;
|
|
3542
|
+
}
|
|
3543
|
+
function retiredFrontendShapeError(pluginName, reasons) {
|
|
3544
|
+
return `Plugin '${pluginName}' ships a terraform/ frontend on the retired ADR-0018 \xA72 per-plugin hosting shape: it declares ${reasons.join(", ")}. ADR-0021 \xA72 replaced this \u2014 a user-facing plugin's frontend is served by the shared plugin host directly from the manifest's \`user_frontend\` field (\`{ dir, required_group }\`); it ships \`web/\` and nothing else, with no terraform/ frontend and no deploy-frontend.yml. Update the plugin to the \`user_frontend\` contract (see _skeletons/plugin-template/README.md) before installing it here.`;
|
|
3545
|
+
}
|
|
3507
3546
|
function renderArguments(args, indent) {
|
|
3508
3547
|
const width = Math.max(...args.map(([key]) => key.length));
|
|
3509
3548
|
return args.map(([key, value]) => `${indent}${key.padEnd(width)} = ${value}`).join("\n");
|
|
@@ -9488,6 +9527,10 @@ async function runPluginInstall(target, options, deps) {
|
|
|
9488
9527
|
log.success(
|
|
9489
9528
|
`Manifest valid \u2014 ${manifest.tables.length} table(s), ${manifest.api_routes.length} route(s)`
|
|
9490
9529
|
);
|
|
9530
|
+
const retiredShapeReasons = findRetiredFrontendShape(join33(source.sourceDir, "terraform"));
|
|
9531
|
+
if (retiredShapeReasons.length > 0) {
|
|
9532
|
+
throw new Error(retiredFrontendShapeError(pluginName, retiredShapeReasons));
|
|
9533
|
+
}
|
|
9491
9534
|
if (inTreeSource) {
|
|
9492
9535
|
log.info(`${relTargetDir}/ is already in this checkout \u2014 installing in place.`);
|
|
9493
9536
|
} else {
|
|
@@ -10256,6 +10299,10 @@ async function runPluginUpgrade(target, options, deps) {
|
|
|
10256
10299
|
log.success(
|
|
10257
10300
|
`Manifest valid \u2014 ${manifest.tables.length} table(s), ${manifest.api_routes.length} route(s)`
|
|
10258
10301
|
);
|
|
10302
|
+
const retiredShapeReasons = findRetiredFrontendShape(join38(tmpDir, "terraform"));
|
|
10303
|
+
if (retiredShapeReasons.length > 0) {
|
|
10304
|
+
throw new Error(retiredFrontendShapeError(entry.name, retiredShapeReasons));
|
|
10305
|
+
}
|
|
10259
10306
|
if (!existsSync36(join38(tmpDir, "terraform"))) {
|
|
10260
10307
|
refuseIfModuleStillReferenced(options.cwd, modulesDir, entry.name);
|
|
10261
10308
|
}
|
|
@@ -10370,6 +10417,10 @@ async function runLocalPluginRefresh(localPath, options, deps) {
|
|
|
10370
10417
|
log.success(
|
|
10371
10418
|
`Manifest valid \u2014 ${manifest.tables.length} table(s), ${manifest.api_routes.length} route(s)`
|
|
10372
10419
|
);
|
|
10420
|
+
const retiredShapeReasons = findRetiredFrontendShape(join38(source.sourceDir, "terraform"));
|
|
10421
|
+
if (retiredShapeReasons.length > 0) {
|
|
10422
|
+
throw new Error(retiredFrontendShapeError(source.name, retiredShapeReasons));
|
|
10423
|
+
}
|
|
10373
10424
|
if (!existsSync36(join38(source.sourceDir, "terraform"))) {
|
|
10374
10425
|
refuseIfModuleStillReferenced(options.cwd, modulesDir, source.name);
|
|
10375
10426
|
}
|
|
@@ -12335,8 +12386,8 @@ async function runOwnershipCheck(argv) {
|
|
|
12335
12386
|
const { stdout } = await execa("git", ["diff", "--cached", "--name-status"], { cwd: root });
|
|
12336
12387
|
({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
|
|
12337
12388
|
if (messageFile) {
|
|
12338
|
-
const { readFileSync:
|
|
12339
|
-
if (existsSync59(messageFile)) commitMessage =
|
|
12389
|
+
const { readFileSync: readFileSync48, existsSync: existsSync59 } = await import("fs");
|
|
12390
|
+
if (existsSync59(messageFile)) commitMessage = readFileSync48(messageFile, "utf8");
|
|
12340
12391
|
}
|
|
12341
12392
|
} else {
|
|
12342
12393
|
const base = process.env["GITHUB_BASE_REF"] ?? args[0];
|
|
@@ -13396,9 +13447,244 @@ baseline was ${String(ratchet.baseline)}, now ${String(ratchet.count)}. See biff
|
|
|
13396
13447
|
}
|
|
13397
13448
|
}
|
|
13398
13449
|
|
|
13450
|
+
// src/lib/ownership-header-claim-guard.ts
|
|
13451
|
+
import { readFileSync as readFileSync39 } from "fs";
|
|
13452
|
+
import { join as join58 } from "path";
|
|
13453
|
+
var OWNERSHIP_HEADER_SWEEP_DIRS = [
|
|
13454
|
+
"scripts",
|
|
13455
|
+
".githooks",
|
|
13456
|
+
".github",
|
|
13457
|
+
"services",
|
|
13458
|
+
"cli",
|
|
13459
|
+
"infra"
|
|
13460
|
+
];
|
|
13461
|
+
var STRICT_MARKER = /(?<!`)(?:INSTANCE-OWNED|NOT a template file)(?!`)/;
|
|
13462
|
+
var SELF_REFERENTIAL_CLAIM = /\b(?:[Tt]his|[Ii]t)\b(?:\s+\S+){0,3}\s+(?:is|are|stays|remains)\s+(?:\*\*)?(template-owned|user-owned)\b/;
|
|
13463
|
+
var EM_DASH_FIRST_LINE_CLAIM = /[—–]\s*(template-owned|user-owned)\b/;
|
|
13464
|
+
var MAX_HEADER_LINES = 10;
|
|
13465
|
+
function claimFor(matchedPhrase) {
|
|
13466
|
+
return /^template-owned$/i.test(matchedPhrase) ? "template" : "user";
|
|
13467
|
+
}
|
|
13468
|
+
function claimInText(text) {
|
|
13469
|
+
const candidates = [];
|
|
13470
|
+
const strict = STRICT_MARKER.exec(text);
|
|
13471
|
+
if (strict)
|
|
13472
|
+
candidates.push({ matchedPhrase: strict[0], claim: claimFor(strict[0]), index: strict.index });
|
|
13473
|
+
const grammatical = SELF_REFERENTIAL_CLAIM.exec(text);
|
|
13474
|
+
if (grammatical) {
|
|
13475
|
+
const phrase = grammatical[1];
|
|
13476
|
+
candidates.push({ matchedPhrase: phrase, claim: claimFor(phrase), index: grammatical.index });
|
|
13477
|
+
}
|
|
13478
|
+
const firstLine = text.split("\n", 1)[0] ?? "";
|
|
13479
|
+
const emDash = EM_DASH_FIRST_LINE_CLAIM.exec(firstLine);
|
|
13480
|
+
if (emDash) {
|
|
13481
|
+
const phrase = emDash[1];
|
|
13482
|
+
candidates.push({ matchedPhrase: phrase, claim: claimFor(phrase), index: emDash.index });
|
|
13483
|
+
}
|
|
13484
|
+
if (candidates.length === 0) return null;
|
|
13485
|
+
candidates.sort((a, b) => a.index - b.index);
|
|
13486
|
+
return candidates[0] ?? null;
|
|
13487
|
+
}
|
|
13488
|
+
function fileExtension(path) {
|
|
13489
|
+
const dot = path.lastIndexOf(".");
|
|
13490
|
+
const slash = path.lastIndexOf("/");
|
|
13491
|
+
return dot > slash ? path.slice(dot).toLowerCase() : "";
|
|
13492
|
+
}
|
|
13493
|
+
function isCommentLine(line) {
|
|
13494
|
+
return line.trim().startsWith("#");
|
|
13495
|
+
}
|
|
13496
|
+
function lineAt(lines, i) {
|
|
13497
|
+
return lines[i] ?? "";
|
|
13498
|
+
}
|
|
13499
|
+
function leadingHashCommentEnd(lines, start) {
|
|
13500
|
+
let i = start;
|
|
13501
|
+
while (i < lines.length) {
|
|
13502
|
+
if (isCommentLine(lineAt(lines, i))) {
|
|
13503
|
+
i++;
|
|
13504
|
+
continue;
|
|
13505
|
+
}
|
|
13506
|
+
if (lineAt(lines, i).trim() === "") {
|
|
13507
|
+
let j = i + 1;
|
|
13508
|
+
while (j < lines.length && lineAt(lines, j).trim() === "") j++;
|
|
13509
|
+
if (j < lines.length && isCommentLine(lineAt(lines, j))) {
|
|
13510
|
+
i = j;
|
|
13511
|
+
continue;
|
|
13512
|
+
}
|
|
13513
|
+
return i;
|
|
13514
|
+
}
|
|
13515
|
+
return i;
|
|
13516
|
+
}
|
|
13517
|
+
return i;
|
|
13518
|
+
}
|
|
13519
|
+
function defaultHeaderRange(lines) {
|
|
13520
|
+
const start = lineAt(lines, 0).startsWith("#!") ? 1 : 0;
|
|
13521
|
+
const end = leadingHashCommentEnd(lines, start);
|
|
13522
|
+
return end > start ? [start, end] : null;
|
|
13523
|
+
}
|
|
13524
|
+
function pythonHeaderRange(lines) {
|
|
13525
|
+
let i = lineAt(lines, 0).startsWith("#!") ? 1 : 0;
|
|
13526
|
+
while (i < lines.length && lineAt(lines, i).trim() === "") i++;
|
|
13527
|
+
if (i >= lines.length) return null;
|
|
13528
|
+
const first = lineAt(lines, i);
|
|
13529
|
+
const trimmed = first.trimStart();
|
|
13530
|
+
const quote = trimmed.startsWith('"""') ? '"""' : trimmed.startsWith("'''") ? "'''" : null;
|
|
13531
|
+
if (quote) {
|
|
13532
|
+
const afterOpen = first.slice(first.indexOf(quote) + 3);
|
|
13533
|
+
if (afterOpen.includes(quote)) return [i, i + 1];
|
|
13534
|
+
let j = i + 1;
|
|
13535
|
+
while (j < lines.length && !lineAt(lines, j).includes(quote)) j++;
|
|
13536
|
+
return [i, Math.min(j + 1, lines.length)];
|
|
13537
|
+
}
|
|
13538
|
+
const end = leadingHashCommentEnd(lines, i);
|
|
13539
|
+
return end > i ? [i, end] : null;
|
|
13540
|
+
}
|
|
13541
|
+
function jsHeaderRange(lines) {
|
|
13542
|
+
let i = 0;
|
|
13543
|
+
while (i < lines.length && (lineAt(lines, i).trim() === "" || lineAt(lines, i).trim().startsWith("import "))) {
|
|
13544
|
+
i++;
|
|
13545
|
+
}
|
|
13546
|
+
if (i >= lines.length) return null;
|
|
13547
|
+
const first = lineAt(lines, i).trim();
|
|
13548
|
+
if (first.startsWith("/*")) {
|
|
13549
|
+
let j = i;
|
|
13550
|
+
while (j < lines.length && !lineAt(lines, j).includes("*/")) j++;
|
|
13551
|
+
return [i, Math.min(j + 1, lines.length)];
|
|
13552
|
+
}
|
|
13553
|
+
if (first.startsWith("//")) {
|
|
13554
|
+
let j = i;
|
|
13555
|
+
while (j < lines.length && (lineAt(lines, j).trim().startsWith("//") || lineAt(lines, j).trim() === ""))
|
|
13556
|
+
j++;
|
|
13557
|
+
return [i, j];
|
|
13558
|
+
}
|
|
13559
|
+
return null;
|
|
13560
|
+
}
|
|
13561
|
+
function headerRange(path, lines) {
|
|
13562
|
+
const ext = fileExtension(path);
|
|
13563
|
+
let range;
|
|
13564
|
+
if (ext === ".py") range = pythonHeaderRange(lines);
|
|
13565
|
+
else if (ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".mjs")
|
|
13566
|
+
range = jsHeaderRange(lines);
|
|
13567
|
+
else if (ext === ".md")
|
|
13568
|
+
range = [0, Math.min(15, lines.length)];
|
|
13569
|
+
else if (ext === ".json")
|
|
13570
|
+
return null;
|
|
13571
|
+
else range = defaultHeaderRange(lines);
|
|
13572
|
+
if (!range) return null;
|
|
13573
|
+
const [start, end] = range;
|
|
13574
|
+
return [start, Math.min(end, start + MAX_HEADER_LINES)];
|
|
13575
|
+
}
|
|
13576
|
+
function findJsonDescriptionClaim(path, content) {
|
|
13577
|
+
let data;
|
|
13578
|
+
try {
|
|
13579
|
+
data = JSON.parse(content);
|
|
13580
|
+
} catch {
|
|
13581
|
+
return null;
|
|
13582
|
+
}
|
|
13583
|
+
if (typeof data !== "object" || data === null) return null;
|
|
13584
|
+
const description = data["description"];
|
|
13585
|
+
if (typeof description !== "string") return null;
|
|
13586
|
+
const match = /INSTANCE-OWNED|NOT a template file|user-owned|template-owned/i.exec(description);
|
|
13587
|
+
if (!match) return null;
|
|
13588
|
+
const lineIndex = content.split("\n").findIndex((l) => l.includes('"description"'));
|
|
13589
|
+
return {
|
|
13590
|
+
path,
|
|
13591
|
+
claim: claimFor(match[0]),
|
|
13592
|
+
matchedPhrase: match[0],
|
|
13593
|
+
line: lineIndex === -1 ? 1 : lineIndex + 1
|
|
13594
|
+
};
|
|
13595
|
+
}
|
|
13596
|
+
function findHeaderClaim(path, content) {
|
|
13597
|
+
if (fileExtension(path) === ".json") return findJsonDescriptionClaim(path, content);
|
|
13598
|
+
const lines = content.split("\n");
|
|
13599
|
+
const range = headerRange(path, lines);
|
|
13600
|
+
if (!range) return null;
|
|
13601
|
+
const [start, end] = range;
|
|
13602
|
+
const windowText = lines.slice(start, end).join("\n");
|
|
13603
|
+
const found = claimInText(windowText);
|
|
13604
|
+
if (!found) return null;
|
|
13605
|
+
const line = start + windowText.slice(0, found.index).split("\n").length;
|
|
13606
|
+
return { path, claim: found.claim, matchedPhrase: found.matchedPhrase, line };
|
|
13607
|
+
}
|
|
13608
|
+
function sweepOwnershipHeaderClaims(root, options = {}) {
|
|
13609
|
+
const tracked = options.git ? gitTrackedFiles(root, options.git) : gitTrackedFiles(root);
|
|
13610
|
+
if (!tracked) return [];
|
|
13611
|
+
const files = [...tracked].filter((p) => OWNERSHIP_HEADER_SWEEP_DIRS.some((dir) => p === dir || p.startsWith(`${dir}/`))).sort();
|
|
13612
|
+
const hits = [];
|
|
13613
|
+
for (const rel of files) {
|
|
13614
|
+
let content;
|
|
13615
|
+
try {
|
|
13616
|
+
content = readFileSync39(join58(root, rel), "utf8");
|
|
13617
|
+
} catch {
|
|
13618
|
+
continue;
|
|
13619
|
+
}
|
|
13620
|
+
const hit = findHeaderClaim(rel, content);
|
|
13621
|
+
if (hit) hits.push(hit);
|
|
13622
|
+
}
|
|
13623
|
+
return hits;
|
|
13624
|
+
}
|
|
13625
|
+
function matchesReleased(relPath, manifest) {
|
|
13626
|
+
return (manifest.released ?? []).some((p) => relPath === p || relPath.startsWith(p));
|
|
13627
|
+
}
|
|
13628
|
+
function checkOwnershipHeaderClaims(hits, manifest) {
|
|
13629
|
+
const out = [];
|
|
13630
|
+
for (const hit of hits) {
|
|
13631
|
+
const manifestSaysTemplateOwned = isTemplateOwned(hit.path, manifest);
|
|
13632
|
+
const released = matchesReleased(hit.path, manifest);
|
|
13633
|
+
const agrees = hit.claim === "template" ? manifestSaysTemplateOwned || released : !manifestSaysTemplateOwned && !released;
|
|
13634
|
+
if (!agrees) {
|
|
13635
|
+
out.push({ ...hit, manifestSaysTemplateOwned });
|
|
13636
|
+
}
|
|
13637
|
+
}
|
|
13638
|
+
return out;
|
|
13639
|
+
}
|
|
13640
|
+
|
|
13641
|
+
// src/scripts/check-ownership-header-claim.ts
|
|
13642
|
+
async function runOwnershipHeaderClaimCheck() {
|
|
13643
|
+
const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
13644
|
+
const ownership = classifyRepoOwnership(root);
|
|
13645
|
+
if (ownership === "satellite") {
|
|
13646
|
+
console.log(
|
|
13647
|
+
"\u2713 ownership-header-claim guard: skipped \u2014 this repo carries no core-manifest.json, so there is no ownership authority to check a header claim against."
|
|
13648
|
+
);
|
|
13649
|
+
return;
|
|
13650
|
+
}
|
|
13651
|
+
const manifest = readCoreManifest(root);
|
|
13652
|
+
const hits = sweepOwnershipHeaderClaims(root);
|
|
13653
|
+
console.log(
|
|
13654
|
+
`examined ${hits.length} file(s) carrying an ownership-style header claim under scripts/, .githooks/, .github/, services/, cli/, infra/`
|
|
13655
|
+
);
|
|
13656
|
+
if (hits.length === 0) {
|
|
13657
|
+
console.error(
|
|
13658
|
+
"\u2717 ownership-header-claim guard: swept zero files \u2014 refusing a false green rather than assuming every ownership-style header vanished."
|
|
13659
|
+
);
|
|
13660
|
+
process.exit(1);
|
|
13661
|
+
return;
|
|
13662
|
+
}
|
|
13663
|
+
const disagreements = checkOwnershipHeaderClaims(hits, manifest);
|
|
13664
|
+
if (disagreements.length > 0) {
|
|
13665
|
+
console.error(
|
|
13666
|
+
`\u2717 ownership-header-claim guard: ${disagreements.length} of ${hits.length} file(s) carry a header claim core-manifest.json disagrees with (#1911):
|
|
13667
|
+
`
|
|
13668
|
+
);
|
|
13669
|
+
for (const d of disagreements) {
|
|
13670
|
+
console.error(
|
|
13671
|
+
` ${d.path}:${d.line} header says "${d.matchedPhrase}" (${d.claim}-owned), but core-manifest.json says ${d.manifestSaysTemplateOwned ? "template-owned" : "user-owned"}`
|
|
13672
|
+
);
|
|
13673
|
+
}
|
|
13674
|
+
console.error(
|
|
13675
|
+
"\nEither fix the header to match core-manifest.json, or fix core-manifest.json to match reality \u2014 never leave the two disagreeing."
|
|
13676
|
+
);
|
|
13677
|
+
process.exit(1);
|
|
13678
|
+
return;
|
|
13679
|
+
}
|
|
13680
|
+
console.log(
|
|
13681
|
+
`\u2713 ownership-header-claim guard: examined ${hits.length} file(s), all agree with core-manifest.json`
|
|
13682
|
+
);
|
|
13683
|
+
}
|
|
13684
|
+
|
|
13399
13685
|
// src/scripts/check-pipe-trap.ts
|
|
13400
|
-
import { readFileSync as
|
|
13401
|
-
import { join as
|
|
13686
|
+
import { readFileSync as readFileSync40, readdirSync as readdirSync23 } from "fs";
|
|
13687
|
+
import { join as join59, relative as relative9 } from "path";
|
|
13402
13688
|
|
|
13403
13689
|
// src/lib/pipe-trap-guard.ts
|
|
13404
13690
|
var STATUS_BEARING = [
|
|
@@ -13494,7 +13780,7 @@ function findPipeTraps(source) {
|
|
|
13494
13780
|
function shellFiles(root) {
|
|
13495
13781
|
const out = [];
|
|
13496
13782
|
for (const dir of ["scripts", ".githooks"]) {
|
|
13497
|
-
const full =
|
|
13783
|
+
const full = join59(root, dir);
|
|
13498
13784
|
let entries;
|
|
13499
13785
|
try {
|
|
13500
13786
|
entries = readdirSync23(full, { withFileTypes: true });
|
|
@@ -13504,7 +13790,7 @@ function shellFiles(root) {
|
|
|
13504
13790
|
for (const entry of entries) {
|
|
13505
13791
|
if (!entry.isFile()) continue;
|
|
13506
13792
|
if (dir === "scripts" && !entry.name.endsWith(".sh")) continue;
|
|
13507
|
-
out.push(
|
|
13793
|
+
out.push(join59(full, entry.name));
|
|
13508
13794
|
}
|
|
13509
13795
|
}
|
|
13510
13796
|
return out;
|
|
@@ -13520,7 +13806,7 @@ async function runPipeTrapCheck() {
|
|
|
13520
13806
|
process.exit(1);
|
|
13521
13807
|
}
|
|
13522
13808
|
const findings = files.flatMap(
|
|
13523
|
-
(file) => findPipeTraps(
|
|
13809
|
+
(file) => findPipeTraps(readFileSync40(file, "utf8")).map(
|
|
13524
13810
|
(t) => `${relative9(root, file)}:${t.line} ${t.text}
|
|
13525
13811
|
${t.reason}`
|
|
13526
13812
|
)
|
|
@@ -13538,11 +13824,11 @@ async function runPipeTrapCheck() {
|
|
|
13538
13824
|
|
|
13539
13825
|
// src/scripts/check-plugin-allowlist-convention.ts
|
|
13540
13826
|
import { existsSync as existsSync50 } from "fs";
|
|
13541
|
-
import { join as
|
|
13827
|
+
import { join as join61 } from "path";
|
|
13542
13828
|
|
|
13543
13829
|
// src/lib/plugin-allowlist-convention.ts
|
|
13544
|
-
import { readFileSync as
|
|
13545
|
-
import { join as
|
|
13830
|
+
import { readFileSync as readFileSync41 } from "fs";
|
|
13831
|
+
import { join as join60 } from "path";
|
|
13546
13832
|
var COMPUTE_MAIN_TF = "modules/cloud/aws/compute/main.tf";
|
|
13547
13833
|
var PLUGIN_TEMPLATE_MAIN_TF = "modules/plugins/_template/main.tf";
|
|
13548
13834
|
var ALLOWLIST_MAIN_TF = "modules/cloud/aws/plugin-allowlist/main.tf";
|
|
@@ -13553,7 +13839,7 @@ var PLUGIN = "<plugin>";
|
|
|
13553
13839
|
var ACCOUNT = "<account>";
|
|
13554
13840
|
function read(repoRoot, relative11) {
|
|
13555
13841
|
try {
|
|
13556
|
-
return
|
|
13842
|
+
return readFileSync41(join60(repoRoot, relative11), "utf8");
|
|
13557
13843
|
} catch {
|
|
13558
13844
|
throw new Error(`plugin-allowlist drift guard: cannot read ${relative11}`);
|
|
13559
13845
|
}
|
|
@@ -13656,7 +13942,7 @@ var REQUIRED_SOURCES = [
|
|
|
13656
13942
|
];
|
|
13657
13943
|
async function runPluginAllowlistConventionCheck() {
|
|
13658
13944
|
const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
13659
|
-
const missing = REQUIRED_SOURCES.filter((relative11) => !existsSync50(
|
|
13945
|
+
const missing = REQUIRED_SOURCES.filter((relative11) => !existsSync50(join61(root, relative11)));
|
|
13660
13946
|
if (missing.length === REQUIRED_SOURCES.length) {
|
|
13661
13947
|
console.log(
|
|
13662
13948
|
`\xB7 Plugin-allowlist convention guard: not applicable \u2014 ${missing.join(", ")} not found under ${root}. This is not a template/instance tree (a satellite repo never carries the plugin-allowlist Terraform modules). Skipping.`
|
|
@@ -13688,11 +13974,11 @@ async function runPluginAllowlistConventionCheck() {
|
|
|
13688
13974
|
|
|
13689
13975
|
// src/scripts/check-plugin-collisions.ts
|
|
13690
13976
|
import { existsSync as existsSync52 } from "fs";
|
|
13691
|
-
import { join as
|
|
13977
|
+
import { join as join63 } from "path";
|
|
13692
13978
|
|
|
13693
13979
|
// src/lib/plugin-collision-guard.ts
|
|
13694
13980
|
import { existsSync as existsSync51, readdirSync as readdirSync24, statSync as statSync15 } from "fs";
|
|
13695
|
-
import { join as
|
|
13981
|
+
import { join as join62 } from "path";
|
|
13696
13982
|
var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
|
|
13697
13983
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
|
|
13698
13984
|
function subdirectories(dir) {
|
|
@@ -13700,19 +13986,19 @@ function subdirectories(dir) {
|
|
|
13700
13986
|
return readdirSync24(dir).filter((entry) => {
|
|
13701
13987
|
if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
|
|
13702
13988
|
try {
|
|
13703
|
-
return statSync15(
|
|
13989
|
+
return statSync15(join62(dir, entry)).isDirectory();
|
|
13704
13990
|
} catch {
|
|
13705
13991
|
return false;
|
|
13706
13992
|
}
|
|
13707
13993
|
});
|
|
13708
13994
|
}
|
|
13709
13995
|
function regularPackagesOf(pluginDir2) {
|
|
13710
|
-
return subdirectories(pluginDir2).filter((name) => existsSync51(
|
|
13996
|
+
return subdirectories(pluginDir2).filter((name) => existsSync51(join62(pluginDir2, name, "__init__.py"))).sort();
|
|
13711
13997
|
}
|
|
13712
13998
|
function bareTestModulesOf(pluginDir2) {
|
|
13713
|
-
const testsDir =
|
|
13999
|
+
const testsDir = join62(pluginDir2, "tests");
|
|
13714
14000
|
if (!existsSync51(testsDir)) return [];
|
|
13715
|
-
if (existsSync51(
|
|
14001
|
+
if (existsSync51(join62(testsDir, "__init__.py"))) return [];
|
|
13716
14002
|
return readdirSync24(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
|
|
13717
14003
|
}
|
|
13718
14004
|
function findCollisions(servicesDir, pluginDirs) {
|
|
@@ -13721,7 +14007,7 @@ function findCollisions(servicesDir, pluginDirs) {
|
|
|
13721
14007
|
const gather = (kind, namesOf) => {
|
|
13722
14008
|
const claims = /* @__PURE__ */ new Map();
|
|
13723
14009
|
for (const plugin of plugins) {
|
|
13724
|
-
for (const name of namesOf(
|
|
14010
|
+
for (const name of namesOf(join62(servicesDir, plugin))) {
|
|
13725
14011
|
claims.set(name, [...claims.get(name) ?? [], plugin]);
|
|
13726
14012
|
}
|
|
13727
14013
|
}
|
|
@@ -13759,7 +14045,7 @@ function formatCollisions(collisions) {
|
|
|
13759
14045
|
// src/scripts/check-plugin-collisions.ts
|
|
13760
14046
|
async function runPluginCollisionCheck() {
|
|
13761
14047
|
const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
13762
|
-
const servicesDir =
|
|
14048
|
+
const servicesDir = join63(root, "services");
|
|
13763
14049
|
if (!existsSync52(servicesDir)) {
|
|
13764
14050
|
console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
|
|
13765
14051
|
return;
|
|
@@ -13777,8 +14063,8 @@ async function runPluginCollisionCheck() {
|
|
|
13777
14063
|
}
|
|
13778
14064
|
|
|
13779
14065
|
// src/lib/plugin-terraform-guard.ts
|
|
13780
|
-
import { existsSync as existsSync53, readFileSync as
|
|
13781
|
-
import { dirname as dirname10, join as
|
|
14066
|
+
import { existsSync as existsSync53, readFileSync as readFileSync42, readdirSync as readdirSync25 } from "fs";
|
|
14067
|
+
import { dirname as dirname10, join as join64, relative as relative10, sep as sep4 } from "path";
|
|
13782
14068
|
var SKIP_DIRS4 = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
|
|
13783
14069
|
var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
|
|
13784
14070
|
function findPluginManifests(root) {
|
|
@@ -13793,9 +14079,9 @@ function findPluginManifests(root) {
|
|
|
13793
14079
|
for (const entry of entries) {
|
|
13794
14080
|
if (entry.isDirectory()) {
|
|
13795
14081
|
if (SKIP_DIRS4.has(entry.name)) continue;
|
|
13796
|
-
walk2(
|
|
14082
|
+
walk2(join64(dir, entry.name));
|
|
13797
14083
|
} else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
|
|
13798
|
-
found.push(relative10(root,
|
|
14084
|
+
found.push(relative10(root, join64(dir, entry.name)).split(sep4).join("/"));
|
|
13799
14085
|
}
|
|
13800
14086
|
}
|
|
13801
14087
|
};
|
|
@@ -13805,7 +14091,7 @@ function findPluginManifests(root) {
|
|
|
13805
14091
|
function readSubscriptions(absManifestPath) {
|
|
13806
14092
|
let parsed;
|
|
13807
14093
|
try {
|
|
13808
|
-
parsed = JSON.parse(
|
|
14094
|
+
parsed = JSON.parse(readFileSync42(absManifestPath, "utf8"));
|
|
13809
14095
|
} catch {
|
|
13810
14096
|
return null;
|
|
13811
14097
|
}
|
|
@@ -13820,14 +14106,14 @@ function readSubscriptions(absManifestPath) {
|
|
|
13820
14106
|
}
|
|
13821
14107
|
function checkPluginTerraform(root) {
|
|
13822
14108
|
const violations = [];
|
|
13823
|
-
const coreManifest = existsSync53(
|
|
14109
|
+
const coreManifest = existsSync53(join64(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
|
|
13824
14110
|
for (const manifest of findPluginManifests(root)) {
|
|
13825
14111
|
if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
|
|
13826
|
-
const absManifest =
|
|
14112
|
+
const absManifest = join64(root, manifest);
|
|
13827
14113
|
const subscriptions = readSubscriptions(absManifest);
|
|
13828
14114
|
if (subscriptions === null) continue;
|
|
13829
14115
|
const pluginDir2 = dirname10(absManifest);
|
|
13830
|
-
if (existsSync53(
|
|
14116
|
+
if (existsSync53(join64(pluginDir2, "terraform"))) continue;
|
|
13831
14117
|
const relPluginDir = relative10(root, pluginDir2).split(sep4).join("/");
|
|
13832
14118
|
violations.push({
|
|
13833
14119
|
manifest,
|
|
@@ -13859,11 +14145,11 @@ async function runPluginTerraformCheck() {
|
|
|
13859
14145
|
|
|
13860
14146
|
// src/scripts/check-plugin-tool-supply.ts
|
|
13861
14147
|
import { existsSync as existsSync55 } from "fs";
|
|
13862
|
-
import { join as
|
|
14148
|
+
import { join as join66 } from "path";
|
|
13863
14149
|
|
|
13864
14150
|
// src/lib/plugin-tool-supply-audit.ts
|
|
13865
|
-
import { existsSync as existsSync54, readFileSync as
|
|
13866
|
-
import { join as
|
|
14151
|
+
import { existsSync as existsSync54, readFileSync as readFileSync43, readdirSync as readdirSync26, statSync as statSync16 } from "fs";
|
|
14152
|
+
import { join as join65 } from "path";
|
|
13867
14153
|
|
|
13868
14154
|
// src/lib/openrouter-model-snapshot.ts
|
|
13869
14155
|
var OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT = "2026-08-10T06:39:01Z";
|
|
@@ -14280,7 +14566,7 @@ function listDirs(root) {
|
|
|
14280
14566
|
}
|
|
14281
14567
|
return entries.filter((e) => {
|
|
14282
14568
|
try {
|
|
14283
|
-
return statSync16(
|
|
14569
|
+
return statSync16(join65(root, e)).isDirectory();
|
|
14284
14570
|
} catch {
|
|
14285
14571
|
return false;
|
|
14286
14572
|
}
|
|
@@ -14296,7 +14582,7 @@ function walkFiles2(root, accept, skipDir) {
|
|
|
14296
14582
|
return;
|
|
14297
14583
|
}
|
|
14298
14584
|
for (const entry of entries) {
|
|
14299
|
-
const p =
|
|
14585
|
+
const p = join65(dir, entry);
|
|
14300
14586
|
let st;
|
|
14301
14587
|
try {
|
|
14302
14588
|
st = statSync16(p);
|
|
@@ -14322,14 +14608,14 @@ function pluginPythonFiles(pluginDir2) {
|
|
|
14322
14608
|
);
|
|
14323
14609
|
}
|
|
14324
14610
|
function pluginTerraformFiles(pluginDir2) {
|
|
14325
|
-
const tfDir =
|
|
14611
|
+
const tfDir = join65(pluginDir2, "terraform");
|
|
14326
14612
|
let entries;
|
|
14327
14613
|
try {
|
|
14328
14614
|
entries = readdirSync26(tfDir);
|
|
14329
14615
|
} catch {
|
|
14330
14616
|
return [];
|
|
14331
14617
|
}
|
|
14332
|
-
return entries.filter((e) => e.endsWith(".tf")).map((e) =>
|
|
14618
|
+
return entries.filter((e) => e.endsWith(".tf")).map((e) => join65(tfDir, e)).sort();
|
|
14333
14619
|
}
|
|
14334
14620
|
function extractManifestTools(manifestText) {
|
|
14335
14621
|
let parsed;
|
|
@@ -14581,8 +14867,8 @@ function isSnapshotStale(fetchedAt, now) {
|
|
|
14581
14867
|
function normalizeModelId(id) {
|
|
14582
14868
|
return id.endsWith(":online") ? id.slice(0, -":online".length) : id;
|
|
14583
14869
|
}
|
|
14584
|
-
var CONFIG_PY_PATH =
|
|
14585
|
-
var ORCHESTRATION_SCHEMA_PATH =
|
|
14870
|
+
var CONFIG_PY_PATH = join65("services", "api", "src", "api", "config.py");
|
|
14871
|
+
var ORCHESTRATION_SCHEMA_PATH = join65(
|
|
14586
14872
|
"services",
|
|
14587
14873
|
"api",
|
|
14588
14874
|
"src",
|
|
@@ -14594,8 +14880,8 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
|
|
|
14594
14880
|
const knownModelIds = options.knownModelIds ?? OPENROUTER_MODEL_IDS;
|
|
14595
14881
|
const snapshotFetchedAt = options.snapshotFetchedAt ?? OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT;
|
|
14596
14882
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
14597
|
-
const configPath =
|
|
14598
|
-
const orchestrationPath =
|
|
14883
|
+
const configPath = join65(repoRoot, CONFIG_PY_PATH);
|
|
14884
|
+
const orchestrationPath = join65(repoRoot, ORCHESTRATION_SCHEMA_PATH);
|
|
14599
14885
|
const configMissing = !existsSync54(configPath);
|
|
14600
14886
|
const orchestrationSchemaMissing = !existsSync54(orchestrationPath);
|
|
14601
14887
|
const knownSet = new Set(knownModelIds);
|
|
@@ -14615,13 +14901,13 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
|
|
|
14615
14901
|
};
|
|
14616
14902
|
let settingsBlind = false;
|
|
14617
14903
|
if (!configMissing) {
|
|
14618
|
-
const settingsFields = extractSettingsModelFields(
|
|
14904
|
+
const settingsFields = extractSettingsModelFields(readFileSync43(configPath, "utf8"));
|
|
14619
14905
|
if (settingsFields.length === 0) settingsBlind = true;
|
|
14620
14906
|
for (const { field, value } of settingsFields) record(`${CONFIG_PY_PATH}#${field}`, value);
|
|
14621
14907
|
}
|
|
14622
14908
|
let curatedFieldsBlind = false;
|
|
14623
14909
|
if (!orchestrationSchemaMissing) {
|
|
14624
|
-
const curated = extractCuratedModelFields(
|
|
14910
|
+
const curated = extractCuratedModelFields(readFileSync43(orchestrationPath, "utf8"));
|
|
14625
14911
|
if (curated.rawFieldCount > 0 && curated.fields.every((f) => f.defaultValue === null && f.optionValues.length === 0)) {
|
|
14626
14912
|
curatedFieldsBlind = true;
|
|
14627
14913
|
}
|
|
@@ -14668,7 +14954,7 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
|
|
|
14668
14954
|
function discoverPluginDirs(pluginsRoot) {
|
|
14669
14955
|
return listDirs(pluginsRoot).filter((name) => {
|
|
14670
14956
|
try {
|
|
14671
|
-
return statSync16(
|
|
14957
|
+
return statSync16(join65(pluginsRoot, name, "biffo.plugin.json")).isFile();
|
|
14672
14958
|
} catch {
|
|
14673
14959
|
return false;
|
|
14674
14960
|
}
|
|
@@ -14681,8 +14967,8 @@ function auditPluginToolSupply(pluginsRoot) {
|
|
|
14681
14967
|
let terraformBlind = false;
|
|
14682
14968
|
let totalDeclaredTools = 0;
|
|
14683
14969
|
for (const name of pluginNames) {
|
|
14684
|
-
const pluginDir2 =
|
|
14685
|
-
const manifestText =
|
|
14970
|
+
const pluginDir2 = join65(pluginsRoot, name);
|
|
14971
|
+
const manifestText = readFileSync43(join65(pluginDir2, "biffo.plugin.json"), "utf8");
|
|
14686
14972
|
const manifest = extractManifestTools(manifestText);
|
|
14687
14973
|
if (manifest.parseError) {
|
|
14688
14974
|
findings.push({
|
|
@@ -14700,13 +14986,13 @@ function auditPluginToolSupply(pluginsRoot) {
|
|
|
14700
14986
|
totalDeclaredTools += manifest.tools.length;
|
|
14701
14987
|
const pySources = pluginPythonFiles(pluginDir2).map((f) => ({
|
|
14702
14988
|
file: f,
|
|
14703
|
-
text:
|
|
14989
|
+
text: readFileSync43(f, "utf8")
|
|
14704
14990
|
}));
|
|
14705
14991
|
const resolver = buildSymbolResolver(pySources);
|
|
14706
14992
|
const registry = extractToolRegistryEntries(pySources, resolver);
|
|
14707
14993
|
if (registry.rawToolDefinitionCount > 0 && registry.entries.length === 0) registryBlind = true;
|
|
14708
14994
|
const tfFiles = pluginTerraformFiles(pluginDir2);
|
|
14709
|
-
const tfText = tfFiles.map((f) =>
|
|
14995
|
+
const tfText = tfFiles.map((f) => readFileSync43(f, "utf8")).join("\n");
|
|
14710
14996
|
const terraform = extractTerraformEnvKeys(tfText);
|
|
14711
14997
|
if (terraform.rawMarkerCount > 0 && terraform.resolvedBlockCount === 0) terraformBlind = true;
|
|
14712
14998
|
for (const toolName of manifest.tools) {
|
|
@@ -14780,7 +15066,7 @@ function auditPluginToolSupply(pluginsRoot) {
|
|
|
14780
15066
|
requiredEnvVars: envResult.envVars,
|
|
14781
15067
|
missingEnvVars: anyWired ? [] : envResult.envVars,
|
|
14782
15068
|
status: anyWired ? "ok" : "missing-env",
|
|
14783
|
-
detail: anyWired ? `${entry.predicate}() is satisfiable: at least one of ${JSON.stringify(envResult.envVars)} is wired in Terraform` : `${entry.predicate}() reads ${JSON.stringify(envResult.envVars)} \u2014 NONE of these are wired by any environment_variables block under ${
|
|
15069
|
+
detail: anyWired ? `${entry.predicate}() is satisfiable: at least one of ${JSON.stringify(envResult.envVars)} is wired in Terraform` : `${entry.predicate}() reads ${JSON.stringify(envResult.envVars)} \u2014 NONE of these are wired by any environment_variables block under ${join65(pluginDir2, "terraform")}, so this deployment can never supply it`
|
|
14784
15070
|
});
|
|
14785
15071
|
}
|
|
14786
15072
|
}
|
|
@@ -14813,7 +15099,7 @@ function auditPluginToolSupply(pluginsRoot) {
|
|
|
14813
15099
|
async function runPluginToolSupplyCheck() {
|
|
14814
15100
|
const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
14815
15101
|
let allOk = true;
|
|
14816
|
-
const pluginsRoot =
|
|
15102
|
+
const pluginsRoot = join66(root, "services", "_plugins");
|
|
14817
15103
|
if (!existsSync55(pluginsRoot)) {
|
|
14818
15104
|
console.log("\u2713 plugin tool-supply guard: no services/_plugins/ \u2014 nothing to audit");
|
|
14819
15105
|
} else {
|
|
@@ -14844,7 +15130,7 @@ async function runPluginToolSupplyCheck() {
|
|
|
14844
15130
|
console.log(`\u2713 plugin tool-supply guard: ${report.summary}`);
|
|
14845
15131
|
}
|
|
14846
15132
|
}
|
|
14847
|
-
const servicesApiRoot =
|
|
15133
|
+
const servicesApiRoot = join66(root, "services", "api");
|
|
14848
15134
|
if (!existsSync55(servicesApiRoot)) {
|
|
14849
15135
|
console.log("\u2713 plugin model-id guard: no services/api/ \u2014 nothing to audit");
|
|
14850
15136
|
} else {
|
|
@@ -15019,10 +15305,10 @@ async function runReleaseSubjectCheck(argv) {
|
|
|
15019
15305
|
}
|
|
15020
15306
|
|
|
15021
15307
|
// src/scripts/check-shared-file-reduction.ts
|
|
15022
|
-
import { readFileSync as
|
|
15308
|
+
import { readFileSync as readFileSync45 } from "fs";
|
|
15023
15309
|
|
|
15024
15310
|
// src/lib/shared-file-reduction-guard.ts
|
|
15025
|
-
import { readFileSync as
|
|
15311
|
+
import { readFileSync as readFileSync44 } from "fs";
|
|
15026
15312
|
var LEAF_TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
|
|
15027
15313
|
var SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite"]);
|
|
15028
15314
|
var TEST_FILE_PATTERN = /\.(test|spec)\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
|
|
@@ -15149,7 +15435,7 @@ function formatReductionReport(report) {
|
|
|
15149
15435
|
// src/scripts/check-shared-file-reduction.ts
|
|
15150
15436
|
function readStdin() {
|
|
15151
15437
|
try {
|
|
15152
|
-
return
|
|
15438
|
+
return readFileSync45(0, "utf8");
|
|
15153
15439
|
} catch {
|
|
15154
15440
|
return "";
|
|
15155
15441
|
}
|
|
@@ -15166,15 +15452,15 @@ function pairsFromTsv(tsv) {
|
|
|
15166
15452
|
const [target, existingPath, incomingPath] = fields;
|
|
15167
15453
|
pairs.push({
|
|
15168
15454
|
target,
|
|
15169
|
-
existing:
|
|
15170
|
-
incoming:
|
|
15455
|
+
existing: readFileSync45(existingPath, "utf8"),
|
|
15456
|
+
incoming: readFileSync45(incomingPath, "utf8")
|
|
15171
15457
|
});
|
|
15172
15458
|
}
|
|
15173
15459
|
return pairs;
|
|
15174
15460
|
}
|
|
15175
15461
|
function loadAccepted(manifestPath) {
|
|
15176
15462
|
if (!manifestPath) return {};
|
|
15177
|
-
const parsed = JSON.parse(
|
|
15463
|
+
const parsed = JSON.parse(readFileSync45(manifestPath, "utf8"));
|
|
15178
15464
|
return parsed.acceptedReductions ?? {};
|
|
15179
15465
|
}
|
|
15180
15466
|
async function runSharedFileReductionCheck(args) {
|
|
@@ -15182,13 +15468,13 @@ async function runSharedFileReductionCheck(args) {
|
|
|
15182
15468
|
let accepted;
|
|
15183
15469
|
try {
|
|
15184
15470
|
if (args.pairs) {
|
|
15185
|
-
pairs = pairsFromTsv(args.pairs === "-" ? readStdin() :
|
|
15471
|
+
pairs = pairsFromTsv(args.pairs === "-" ? readStdin() : readFileSync45(args.pairs, "utf8"));
|
|
15186
15472
|
} else if (args.target && args.existing && args.incoming) {
|
|
15187
15473
|
pairs = [
|
|
15188
15474
|
{
|
|
15189
15475
|
target: args.target,
|
|
15190
|
-
existing:
|
|
15191
|
-
incoming:
|
|
15476
|
+
existing: readFileSync45(args.existing, "utf8"),
|
|
15477
|
+
incoming: readFileSync45(args.incoming, "utf8")
|
|
15192
15478
|
}
|
|
15193
15479
|
];
|
|
15194
15480
|
} else {
|
|
@@ -15223,11 +15509,11 @@ async function runSharedFileReductionCheck(args) {
|
|
|
15223
15509
|
|
|
15224
15510
|
// src/scripts/check-skeleton-drift.ts
|
|
15225
15511
|
import { existsSync as existsSync56, readdirSync as readdirSync28 } from "fs";
|
|
15226
|
-
import { join as
|
|
15512
|
+
import { join as join68 } from "path";
|
|
15227
15513
|
|
|
15228
15514
|
// src/lib/skeleton-drift-guard.ts
|
|
15229
|
-
import { readFileSync as
|
|
15230
|
-
import { join as
|
|
15515
|
+
import { readFileSync as readFileSync46, readdirSync as readdirSync27, statSync as statSync17 } from "fs";
|
|
15516
|
+
import { join as join67 } from "path";
|
|
15231
15517
|
var isWorkflow = (rel) => rel.startsWith(".github/workflows/") && (rel.endsWith(".yml") || rel.endsWith(".yaml"));
|
|
15232
15518
|
var isRootLayout = (rel) => rel.endsWith("src/app/layout.tsx");
|
|
15233
15519
|
var uncommented = (contents) => contents.split("\n").filter((line) => !/^\s*(\/\/|\/\*|\*)/.test(line)).join("\n");
|
|
@@ -15291,7 +15577,7 @@ function walk(dir, base = dir) {
|
|
|
15291
15577
|
}
|
|
15292
15578
|
for (const entry of entries) {
|
|
15293
15579
|
if (entry === ".venv" || entry === "node_modules" || entry === ".git") continue;
|
|
15294
|
-
const abs =
|
|
15580
|
+
const abs = join67(dir, entry);
|
|
15295
15581
|
let isDir;
|
|
15296
15582
|
try {
|
|
15297
15583
|
isDir = statSync17(abs).isDirectory();
|
|
@@ -15313,7 +15599,7 @@ function auditSkeleton(skeletonRoot, name, rules = SKELETON_RULES) {
|
|
|
15313
15599
|
if (!rule.appliesTo(rel)) continue;
|
|
15314
15600
|
let contents;
|
|
15315
15601
|
try {
|
|
15316
|
-
contents =
|
|
15602
|
+
contents = readFileSync46(join67(skeletonRoot, rel), "utf8");
|
|
15317
15603
|
} catch {
|
|
15318
15604
|
continue;
|
|
15319
15605
|
}
|
|
@@ -15342,23 +15628,23 @@ function formatViolations2(violations) {
|
|
|
15342
15628
|
|
|
15343
15629
|
// src/scripts/check-skeleton-drift.ts
|
|
15344
15630
|
function discoverSkeletons(root) {
|
|
15345
|
-
const skeletonsDir =
|
|
15631
|
+
const skeletonsDir = join68(root, "_skeletons");
|
|
15346
15632
|
let entries;
|
|
15347
15633
|
try {
|
|
15348
15634
|
entries = readdirSync28(skeletonsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
15349
15635
|
} catch {
|
|
15350
15636
|
return [];
|
|
15351
15637
|
}
|
|
15352
|
-
return entries.filter((name) => existsSync56(
|
|
15638
|
+
return entries.filter((name) => existsSync56(join68(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
|
|
15353
15639
|
}
|
|
15354
15640
|
async function runSkeletonDriftCheck() {
|
|
15355
15641
|
const root = (await execa("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
15356
15642
|
const skeletons = discoverSkeletons(root);
|
|
15357
15643
|
let filesConsidered = 0;
|
|
15358
15644
|
for (const name of skeletons) {
|
|
15359
|
-
const skeletonRoot =
|
|
15645
|
+
const skeletonRoot = join68(root, "_skeletons", name);
|
|
15360
15646
|
filesConsidered += findWorkflowFiles(skeletonRoot).length;
|
|
15361
|
-
if (existsSync56(
|
|
15647
|
+
if (existsSync56(join68(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
|
|
15362
15648
|
filesConsidered += 1;
|
|
15363
15649
|
}
|
|
15364
15650
|
}
|
|
@@ -15366,7 +15652,7 @@ async function runSkeletonDriftCheck() {
|
|
|
15366
15652
|
`audited ${skeletons.length} skeleton(s) (${skeletons.join(", ") || "none"}), ${filesConsidered} file(s) considered, under ${root}/_skeletons`
|
|
15367
15653
|
);
|
|
15368
15654
|
if (skeletons.length === 0) {
|
|
15369
|
-
if (!existsSync56(
|
|
15655
|
+
if (!existsSync56(join68(root, "_skeletons"))) {
|
|
15370
15656
|
console.log(
|
|
15371
15657
|
`\xB7 Skeleton-drift guard: no _skeletons/ directory under ${root} \u2014 this is not a repo that ships scaffolding (a satellite repo never carries one). Not applicable; skipping.`
|
|
15372
15658
|
);
|
|
@@ -15378,7 +15664,7 @@ async function runSkeletonDriftCheck() {
|
|
|
15378
15664
|
process.exit(1);
|
|
15379
15665
|
}
|
|
15380
15666
|
const violations = skeletons.flatMap(
|
|
15381
|
-
(name) => auditSkeleton(
|
|
15667
|
+
(name) => auditSkeleton(join68(root, "_skeletons", name), name)
|
|
15382
15668
|
);
|
|
15383
15669
|
if (violations.length > 0) {
|
|
15384
15670
|
console.error("\u2717 Skeleton-drift guard: drift found between this repo and its scaffolding\n");
|
|
@@ -15414,7 +15700,7 @@ async function runTerraformInputCheck() {
|
|
|
15414
15700
|
|
|
15415
15701
|
// src/commands/check.ts
|
|
15416
15702
|
var checkCommand = new Command24("check").description(
|
|
15417
|
-
"Repo guards (ownership, release subject, plugin terraform, plugin collisions, eventbridge-log-permissions, plugin-tool-supply, core-direct-paths, instance-adoption, orphan-ratchet, cognito-invite-template, lambda-output, pipe-trap, codeql-suppression, skeleton-drift, terraform-input, plugin-allowlist-convention, migration-body-change, distribution-inventory, distribution-remote-state, api-gateway-integration) run in CI and git hooks, plus out-of-band audits (branch protection, plugin-staleness)"
|
|
15703
|
+
"Repo guards (ownership, ownership-header-claim, release subject, plugin terraform, plugin collisions, eventbridge-log-permissions, plugin-tool-supply, core-direct-paths, instance-adoption, orphan-ratchet, cognito-invite-template, lambda-output, pipe-trap, codeql-suppression, skeleton-drift, terraform-input, plugin-allowlist-convention, migration-body-change, distribution-inventory, distribution-remote-state, api-gateway-integration) run in CI and git hooks, plus out-of-band audits (branch protection, plugin-staleness)"
|
|
15418
15704
|
);
|
|
15419
15705
|
checkCommand.command("ownership").description("Refuse changes to template-owned paths in an instance (#370)").argument("[base]", "Base branch to diff against; defaults to $GITHUB_BASE_REF").option("--staged <messageFile>", "Check staged changes instead of a branch diff (commit hook)").allowExcessArguments(true).action(async () => {
|
|
15420
15706
|
await runOwnershipCheck(rawArgsAfter("ownership"));
|
|
@@ -15503,6 +15789,11 @@ checkCommand.command("orphan-ratchet").description(
|
|
|
15503
15789
|
await runOrphanRatchetCheck(opts);
|
|
15504
15790
|
}
|
|
15505
15791
|
);
|
|
15792
|
+
checkCommand.command("ownership-header-claim").description(
|
|
15793
|
+
`Refuse a file whose own header comment claims INSTANCE-OWNED/template-owned/user-owned/"NOT a template file" while core-manifest.json's real longest-prefix-match answer disagrees (#1911) \u2014 scripts/verify-deployed.checks lived this exact gap (#1706/#1707) until a human happened to notice by hand; nothing else compared the two documents.`
|
|
15794
|
+
).action(async () => {
|
|
15795
|
+
await runOwnershipHeaderClaimCheck();
|
|
15796
|
+
});
|
|
15506
15797
|
checkCommand.command("cognito-invite-template").description(
|
|
15507
15798
|
"Refuse a Cognito invite_message_template missing a required member or placeholder (#356) \u2014 terraform validate is silent on this, so a fresh deploy fails on its first apply"
|
|
15508
15799
|
).action(async () => {
|
|
@@ -15574,8 +15865,8 @@ function rawArgsAfter(subcommand) {
|
|
|
15574
15865
|
}
|
|
15575
15866
|
|
|
15576
15867
|
// src/commands/doctor.ts
|
|
15577
|
-
import { existsSync as existsSync57, readFileSync as
|
|
15578
|
-
import { join as
|
|
15868
|
+
import { existsSync as existsSync57, readFileSync as readFileSync47 } from "fs";
|
|
15869
|
+
import { join as join69, resolve as resolve20 } from "path";
|
|
15579
15870
|
import chalk21 from "chalk";
|
|
15580
15871
|
import { Command as Command25 } from "commander";
|
|
15581
15872
|
|
|
@@ -15970,10 +16261,10 @@ async function runDoctorFixBranches(cwd, facts, deps = { git: new GitAdapter(),
|
|
|
15970
16261
|
return reapAllBareBranches(cwd, facts.branches, facts.worktrees, facts.currentBranch, deps);
|
|
15971
16262
|
}
|
|
15972
16263
|
function readLocalCoreVersion(cwd) {
|
|
15973
|
-
const path =
|
|
16264
|
+
const path = join69(cwd, INSTANCE_CORE_FILE);
|
|
15974
16265
|
if (!existsSync57(path)) return null;
|
|
15975
16266
|
try {
|
|
15976
|
-
return extractVersionField(
|
|
16267
|
+
return extractVersionField(readFileSync47(path, "utf8"));
|
|
15977
16268
|
} catch {
|
|
15978
16269
|
return null;
|
|
15979
16270
|
}
|
|
@@ -15993,10 +16284,10 @@ function extractVersionField(contents) {
|
|
|
15993
16284
|
return match?.[1] ?? null;
|
|
15994
16285
|
}
|
|
15995
16286
|
function readFossil(cwd) {
|
|
15996
|
-
const path =
|
|
16287
|
+
const path = join69(cwd, CORE_VERSION_FILE);
|
|
15997
16288
|
if (!existsSync57(path)) return null;
|
|
15998
16289
|
try {
|
|
15999
|
-
const value =
|
|
16290
|
+
const value = readFileSync47(path, "utf8").trim();
|
|
16000
16291
|
return value === "" ? null : value;
|
|
16001
16292
|
} catch {
|
|
16002
16293
|
return null;
|
|
@@ -16526,11 +16817,11 @@ import { Command as Command27 } from "commander";
|
|
|
16526
16817
|
|
|
16527
16818
|
// src/lib/packaged-scripts.ts
|
|
16528
16819
|
import { existsSync as existsSync58 } from "fs";
|
|
16529
|
-
import { dirname as dirname11, join as
|
|
16820
|
+
import { dirname as dirname11, join as join70 } from "path";
|
|
16530
16821
|
function findPackagedScript(startDir, relativePath) {
|
|
16531
16822
|
let dir = startDir;
|
|
16532
16823
|
for (; ; ) {
|
|
16533
|
-
const candidate =
|
|
16824
|
+
const candidate = join70(dir, relativePath);
|
|
16534
16825
|
if (existsSync58(candidate)) return candidate;
|
|
16535
16826
|
const parent = dirname11(dir);
|
|
16536
16827
|
if (parent === dir) return null;
|
package/package.json
CHANGED
package/scripts/verify.sh
CHANGED
|
@@ -1449,6 +1449,7 @@ if [ -f scripts/biffo.sh ]; then
|
|
|
1449
1449
|
run_check plugin-tool-supply sh scripts/biffo.sh check plugin-tool-supply
|
|
1450
1450
|
run_check core-direct-paths sh scripts/biffo.sh check core-direct-paths
|
|
1451
1451
|
run_check orphan-ratchet sh scripts/biffo.sh check orphan-ratchet
|
|
1452
|
+
run_check ownership-header-claim sh scripts/biffo.sh check ownership-header-claim
|
|
1452
1453
|
# #1714 second remediation: the INSTANCE-mode caller, distinct from the
|
|
1453
1454
|
# self-check above. It clones a real template tree pinned to
|
|
1454
1455
|
# biffo.core.json's own version and compares against it -- but this repo
|