@attalabs/vinaya 0.3.0 → 0.4.1
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/aeg-root/enforcement.md +4 -3
- package/aeg-root/glossary.md +4 -0
- package/aeg-root/roles/reviewer.md +1 -1
- package/aeg-root/roles/security.md +1 -1
- package/dist/checks/bin/check-branch-topology.js +7 -3
- package/dist/checks/bin/check-brief-shape.js +7 -3
- package/dist/checks/bin/check-closes-n.js +7 -3
- package/dist/checks/bin/check-coherence.js +7 -3
- package/dist/checks/bin/check-dead-branch-push.js +7 -3
- package/dist/checks/bin/check-dispatch-readiness.js +7 -3
- package/dist/checks/bin/check-doc-coverage-push.js +7 -3
- package/dist/checks/bin/check-doc-coverage.js +7 -3
- package/dist/checks/bin/check-first-push-dispatch.js +7 -3
- package/dist/checks/bin/check-issue-assignment.js +7 -3
- package/dist/checks/bin/check-no-disk-state.js +7 -3
- package/dist/checks/bin/check-reader-resolvable-prose.js +7 -3
- package/dist/checks/bin/check-registry-gates.js +7 -3
- package/dist/checks/bin/check-review-gate.js +12 -4
- package/dist/checks/bin/check-single-plan-pr.js +7 -3
- package/dist/checks/bin/check-test-plan.js +7 -3
- package/dist/index.js +363 -65
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
5
5
|
import { dirname as dirname5, join as join15 } from "node:path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7
7
|
|
|
@@ -1334,7 +1334,9 @@ function coreCheckRegistry() {
|
|
|
1334
1334
|
timeoutMs: 15000,
|
|
1335
1335
|
env: {
|
|
1336
1336
|
BASE_SHA: { optional: true },
|
|
1337
|
-
PR_NUMBER: { optional: true }
|
|
1337
|
+
PR_NUMBER: { optional: true },
|
|
1338
|
+
GITHUB_TOKEN: { optional: true },
|
|
1339
|
+
GH_TOKEN: { optional: true }
|
|
1338
1340
|
}
|
|
1339
1341
|
},
|
|
1340
1342
|
{
|
|
@@ -1359,7 +1361,11 @@ function coreCheckRegistry() {
|
|
|
1359
1361
|
run: bin("check-registry-gates"),
|
|
1360
1362
|
scope: "full",
|
|
1361
1363
|
timeoutMs: 30000,
|
|
1362
|
-
env: {
|
|
1364
|
+
env: {
|
|
1365
|
+
AEG_REPO: { optional: true },
|
|
1366
|
+
GITHUB_TOKEN: { optional: true },
|
|
1367
|
+
GH_TOKEN: { optional: true }
|
|
1368
|
+
}
|
|
1363
1369
|
},
|
|
1364
1370
|
{
|
|
1365
1371
|
name: "review-gate",
|
|
@@ -1368,7 +1374,9 @@ function coreCheckRegistry() {
|
|
|
1368
1374
|
timeoutMs: 30000,
|
|
1369
1375
|
env: {
|
|
1370
1376
|
BRANCH: { optional: true },
|
|
1371
|
-
PR_NUMBER: { optional: true }
|
|
1377
|
+
PR_NUMBER: { optional: true },
|
|
1378
|
+
GITHUB_TOKEN: { optional: true },
|
|
1379
|
+
GH_TOKEN: { optional: true }
|
|
1372
1380
|
}
|
|
1373
1381
|
},
|
|
1374
1382
|
{
|
|
@@ -1378,7 +1386,9 @@ function coreCheckRegistry() {
|
|
|
1378
1386
|
timeoutMs: 30000,
|
|
1379
1387
|
env: {
|
|
1380
1388
|
BRANCH: { optional: true },
|
|
1381
|
-
AEG_REPO: { optional: true }
|
|
1389
|
+
AEG_REPO: { optional: true },
|
|
1390
|
+
GITHUB_TOKEN: { optional: true },
|
|
1391
|
+
GH_TOKEN: { optional: true }
|
|
1382
1392
|
}
|
|
1383
1393
|
},
|
|
1384
1394
|
{
|
|
@@ -1386,7 +1396,11 @@ function coreCheckRegistry() {
|
|
|
1386
1396
|
run: bin("check-dead-branch-push"),
|
|
1387
1397
|
scope: "full",
|
|
1388
1398
|
timeoutMs: 30000,
|
|
1389
|
-
env: {
|
|
1399
|
+
env: {
|
|
1400
|
+
BRANCH: { optional: true },
|
|
1401
|
+
GITHUB_TOKEN: { optional: true },
|
|
1402
|
+
GH_TOKEN: { optional: true }
|
|
1403
|
+
}
|
|
1390
1404
|
},
|
|
1391
1405
|
{
|
|
1392
1406
|
name: "first-push-dispatch",
|
|
@@ -1421,12 +1435,62 @@ function coreCheckRegistry() {
|
|
|
1421
1435
|
timeoutMs: 30000,
|
|
1422
1436
|
env: {
|
|
1423
1437
|
AEG_REPO: { optional: true },
|
|
1424
|
-
BRANCH: { optional: true }
|
|
1438
|
+
BRANCH: { optional: true },
|
|
1439
|
+
GITHUB_TOKEN: { optional: true },
|
|
1440
|
+
GH_TOKEN: { optional: true }
|
|
1425
1441
|
}
|
|
1426
1442
|
}
|
|
1427
1443
|
];
|
|
1428
1444
|
}
|
|
1429
1445
|
|
|
1446
|
+
// src/checks/resolver.ts
|
|
1447
|
+
var NAMESPACE_SEGMENT = /^[a-z0-9][a-z0-9-]*$/;
|
|
1448
|
+
var RESERVED_PREFIX = "vinaya";
|
|
1449
|
+
function isValidNamespacedKey(key) {
|
|
1450
|
+
const parts = key.split("/");
|
|
1451
|
+
if (parts.length !== 2)
|
|
1452
|
+
return false;
|
|
1453
|
+
const [prefix, rest] = parts;
|
|
1454
|
+
if (!prefix || !rest)
|
|
1455
|
+
return false;
|
|
1456
|
+
if (!NAMESPACE_SEGMENT.test(prefix) || !NAMESPACE_SEGMENT.test(rest))
|
|
1457
|
+
return false;
|
|
1458
|
+
if (prefix === RESERVED_PREFIX)
|
|
1459
|
+
return false;
|
|
1460
|
+
return true;
|
|
1461
|
+
}
|
|
1462
|
+
function resolveChecks(core, configChecks) {
|
|
1463
|
+
const resolved = core.map((spec) => ({ name: spec.name, state: "default", source: "core", spec }));
|
|
1464
|
+
const failures = [];
|
|
1465
|
+
if (!configChecks)
|
|
1466
|
+
return { resolved, failures };
|
|
1467
|
+
const indexByName = new Map(resolved.map((entry2, index) => [entry2.name, index]));
|
|
1468
|
+
for (const [key, entry2] of Object.entries(configChecks)) {
|
|
1469
|
+
const coreIndex = indexByName.get(key);
|
|
1470
|
+
if (coreIndex !== undefined) {
|
|
1471
|
+
resolved[coreIndex] = {
|
|
1472
|
+
name: key,
|
|
1473
|
+
state: "overridden",
|
|
1474
|
+
source: "config",
|
|
1475
|
+
spec: { name: key, ...entry2 }
|
|
1476
|
+
};
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
if (isValidNamespacedKey(key)) {
|
|
1480
|
+
resolved.push({ name: key, state: "additive", source: "config", spec: { name: key, ...entry2 } });
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
failures.push({ key, reason: 'bare key has no "/" and matches no core check id' });
|
|
1484
|
+
}
|
|
1485
|
+
return { resolved, failures };
|
|
1486
|
+
}
|
|
1487
|
+
function overriddenNextMinorWarning(name) {
|
|
1488
|
+
return `check "${name}" shares its name with a core check — it will replace the core check starting next minor (currently it runs alongside it under the flat registry).`;
|
|
1489
|
+
}
|
|
1490
|
+
function bareKeyNextMinorWarning(key) {
|
|
1491
|
+
return `check "${key}" has no namespace and matches no core check — it will be rejected starting next minor. Rename it to "<yourname>/x".`;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1430
1494
|
// src/checks/runner.ts
|
|
1431
1495
|
import { spawn } from "node:child_process";
|
|
1432
1496
|
import { cpus } from "node:os";
|
|
@@ -1832,6 +1896,15 @@ function configPath() {
|
|
|
1832
1896
|
return GLOBAL_CONFIG_PATH;
|
|
1833
1897
|
return null;
|
|
1834
1898
|
}
|
|
1899
|
+
function globalChecksIgnoredWarning(path) {
|
|
1900
|
+
return `${path}: "checks" registration in the global config is ignored — checks may only be registered from a repo-local vinaya.config.json.`;
|
|
1901
|
+
}
|
|
1902
|
+
function stripGlobalChecks(config, path) {
|
|
1903
|
+
if (path !== GLOBAL_CONFIG_PATH || !config.checks || Object.keys(config.checks).length === 0)
|
|
1904
|
+
return config;
|
|
1905
|
+
console.error(`⚠ ${globalChecksIgnoredWarning(path)}`);
|
|
1906
|
+
return { ...config, checks: undefined };
|
|
1907
|
+
}
|
|
1835
1908
|
function loadConfigChecked() {
|
|
1836
1909
|
const path = configPath();
|
|
1837
1910
|
if (!path)
|
|
@@ -1847,7 +1920,7 @@ function loadConfigChecked() {
|
|
|
1847
1920
|
const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
1848
1921
|
return { ok: false, path, error: detail };
|
|
1849
1922
|
}
|
|
1850
|
-
return { ok: true, config: parsed.data };
|
|
1923
|
+
return { ok: true, config: stripGlobalChecks(parsed.data, path) };
|
|
1851
1924
|
}
|
|
1852
1925
|
|
|
1853
1926
|
// src/commands/check.ts
|
|
@@ -1887,25 +1960,112 @@ function configErrorOutcome(path, error) {
|
|
|
1887
1960
|
function customSpecsFromConfig() {
|
|
1888
1961
|
const result = loadConfigChecked();
|
|
1889
1962
|
if (!result.ok)
|
|
1890
|
-
return { specs: [], errorOutcome: configErrorOutcome(result.path, result.error) };
|
|
1963
|
+
return { specs: [], errorOutcome: configErrorOutcome(result.path, result.error), checks: undefined };
|
|
1891
1964
|
const checks = result.config?.checks;
|
|
1892
1965
|
if (!checks)
|
|
1893
|
-
return { specs: [], errorOutcome: null };
|
|
1966
|
+
return { specs: [], errorOutcome: null, checks: undefined };
|
|
1894
1967
|
const specs = Object.entries(checks).map(([name, entry2]) => ({ name, ...entry2 }));
|
|
1895
|
-
return { specs, errorOutcome: null };
|
|
1968
|
+
return { specs, errorOutcome: null, checks };
|
|
1969
|
+
}
|
|
1970
|
+
function classifyEnvValue(value) {
|
|
1971
|
+
if (value === true)
|
|
1972
|
+
return "passthrough";
|
|
1973
|
+
if (typeof value === "string")
|
|
1974
|
+
return "literal";
|
|
1975
|
+
if ("anyOf" in value)
|
|
1976
|
+
return "anyOf";
|
|
1977
|
+
return "optional";
|
|
1978
|
+
}
|
|
1979
|
+
function renderPlanJson(result) {
|
|
1980
|
+
const checks = {};
|
|
1981
|
+
for (const entry2 of result.resolved) {
|
|
1982
|
+
const env = {};
|
|
1983
|
+
let envAnyOf;
|
|
1984
|
+
for (const [key, value] of Object.entries(entry2.spec.env ?? {})) {
|
|
1985
|
+
env[key] = classifyEnvValue(value);
|
|
1986
|
+
if (typeof value === "object" && value !== null && "anyOf" in value) {
|
|
1987
|
+
envAnyOf ??= {};
|
|
1988
|
+
envAnyOf[key] = value.anyOf;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
checks[entry2.name] = { state: entry2.state, source: entry2.source, env, envAnyOf, scope: entry2.spec.scope };
|
|
1992
|
+
}
|
|
1993
|
+
return {
|
|
1994
|
+
schema: 1,
|
|
1995
|
+
checks,
|
|
1996
|
+
roles: { available: false, reason: "role resolution not implemented yet — see task 6" },
|
|
1997
|
+
errors: result.failures
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
function envCellFor(spec) {
|
|
2001
|
+
const entries = Object.entries(spec.env ?? {});
|
|
2002
|
+
if (entries.length === 0)
|
|
2003
|
+
return "—";
|
|
2004
|
+
return entries.map(([key, value]) => {
|
|
2005
|
+
if (value === true)
|
|
2006
|
+
return key;
|
|
2007
|
+
if (typeof value === "string")
|
|
2008
|
+
return `${key}=<redacted>`;
|
|
2009
|
+
if ("anyOf" in value)
|
|
2010
|
+
return value.anyOf.join("|");
|
|
2011
|
+
return `${key}?`;
|
|
2012
|
+
}).join(", ");
|
|
2013
|
+
}
|
|
2014
|
+
function renderPlanTable(result) {
|
|
2015
|
+
const header = ["NAME", "STATE", "SOURCE", "ENV"];
|
|
2016
|
+
const rows = result.resolved.map((entry2) => [entry2.name, entry2.state, entry2.source, envCellFor(entry2.spec)]);
|
|
2017
|
+
const failureRows = result.failures.map((f) => [f.key, "FAILED", "—", f.reason]);
|
|
2018
|
+
const table = [header, ...rows, ...failureRows];
|
|
2019
|
+
const widths = header.map((_, col) => Math.max(...table.map((row) => row[col]?.length ?? 0)));
|
|
2020
|
+
return table.map((row) => row.map((cell, col) => (cell ?? "").padEnd(widths[col] ?? 0)).join(" ")).join(`
|
|
2021
|
+
`);
|
|
2022
|
+
}
|
|
2023
|
+
function resolveForPlan(configResult) {
|
|
2024
|
+
if (configResult.ok) {
|
|
2025
|
+
return resolveChecks(coreCheckRegistry(), configResult.config?.checks);
|
|
2026
|
+
}
|
|
2027
|
+
const core = resolveChecks(coreCheckRegistry(), undefined);
|
|
2028
|
+
return {
|
|
2029
|
+
resolved: core.resolved,
|
|
2030
|
+
failures: [{ key: configResult.path, reason: `invalid \`checks\` registration — ${configResult.error}` }]
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
function printClassificationWarnings(configChecks) {
|
|
2034
|
+
const classification = resolveChecks(coreCheckRegistry(), configChecks);
|
|
2035
|
+
for (const entry2 of classification.resolved) {
|
|
2036
|
+
if (entry2.state === "overridden")
|
|
2037
|
+
process.stdout.write(`${overriddenNextMinorWarning(entry2.name)}
|
|
2038
|
+
`);
|
|
2039
|
+
}
|
|
2040
|
+
for (const failure of classification.failures) {
|
|
2041
|
+
process.stdout.write(`${bareKeyNextMinorWarning(failure.key)}
|
|
2042
|
+
`);
|
|
2043
|
+
}
|
|
1896
2044
|
}
|
|
1897
2045
|
async function checkCommand(args) {
|
|
1898
2046
|
const jsonOutput = args.includes("--json");
|
|
1899
2047
|
const diffOnly = args.includes("--diff-only");
|
|
1900
2048
|
const requestedParallel = parseParallel(args);
|
|
1901
2049
|
const allRequested = args.includes("--all");
|
|
2050
|
+
const planRequested = args.includes("--plan");
|
|
1902
2051
|
const positional = args.filter((a) => !a.startsWith("--"));
|
|
1903
2052
|
const requestedName = positional[0];
|
|
2053
|
+
if (planRequested) {
|
|
2054
|
+
const result = resolveForPlan(loadConfigChecked());
|
|
2055
|
+
if (jsonOutput) {
|
|
2056
|
+
process.stdout.write(`${JSON.stringify(renderPlanJson(result), null, 2)}
|
|
2057
|
+
`);
|
|
2058
|
+
} else {
|
|
2059
|
+
process.stdout.write(`${renderPlanTable(result)}
|
|
2060
|
+
`);
|
|
2061
|
+
}
|
|
2062
|
+
process.exit(result.failures.length > 0 ? 1 : 0);
|
|
2063
|
+
}
|
|
1904
2064
|
if (!allRequested && !requestedName) {
|
|
1905
|
-
console.error("Usage: vinaya check <name> | --all [--json] [--diff-only] [--parallel[=n]]");
|
|
2065
|
+
console.error("Usage: vinaya check <name> | --all | --plan [--json] [--diff-only] [--parallel[=n]]");
|
|
1906
2066
|
process.exit(2);
|
|
1907
2067
|
}
|
|
1908
|
-
const { specs: customSpecs, errorOutcome } = customSpecsFromConfig();
|
|
2068
|
+
const { specs: customSpecs, errorOutcome, checks: configChecks } = customSpecsFromConfig();
|
|
1909
2069
|
const allSpecs = [...coreCheckRegistry(), ...customSpecs];
|
|
1910
2070
|
const specsToRun = allRequested ? allSpecs : allSpecs.filter((s) => s.name === requestedName);
|
|
1911
2071
|
if (!allRequested && specsToRun.length === 0) {
|
|
@@ -1935,6 +2095,7 @@ async function checkCommand(args) {
|
|
|
1935
2095
|
process.stdout.write(` ${e.severity}: ${e.message}
|
|
1936
2096
|
`);
|
|
1937
2097
|
}
|
|
2098
|
+
printClassificationWarnings(configChecks);
|
|
1938
2099
|
}
|
|
1939
2100
|
const failed = allOutcomes.some((o) => o.status === "fail" || o.status === "error" || o.status === "timeout");
|
|
1940
2101
|
process.exit(failed ? 1 : 0);
|
|
@@ -2149,15 +2310,17 @@ async function demoBreakCommand(args) {
|
|
|
2149
2310
|
}
|
|
2150
2311
|
|
|
2151
2312
|
// src/commands/doctor.ts
|
|
2152
|
-
import { existsSync as existsSync8, readFileSync as
|
|
2313
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, statSync } from "node:fs";
|
|
2153
2314
|
import { join as join8 } from "node:path";
|
|
2154
2315
|
|
|
2155
2316
|
// src/lib/artifacts.ts
|
|
2317
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
2156
2318
|
import { join as join6 } from "node:path";
|
|
2157
2319
|
var CONFIG_PATH = "vinaya.config.json";
|
|
2158
2320
|
var DOCTRINE_POINTER_PATH = "VINAYA.md";
|
|
2159
2321
|
var CHECKS_WORKFLOW_PATH = ".github/workflows/vinaya-checks.yml";
|
|
2160
2322
|
var REVIEW_WORKFLOW_PATH = ".github/workflows/vinaya-review.yml";
|
|
2323
|
+
var REVIEW_VERDICT_WORKFLOW_PATH = ".github/workflows/vinaya-review-verdict.yml";
|
|
2161
2324
|
var ARCHIVIST_WORKFLOW_PATH = ".github/workflows/vinaya-archivist.yml";
|
|
2162
2325
|
var MANAGED_NOTE = "Managed by Vinaya — created by `vinaya init`. `vinaya upgrade` regenerates it; `vinaya eject` removes it.";
|
|
2163
2326
|
function starterConfig() {
|
|
@@ -2197,6 +2360,8 @@ jobs:
|
|
|
2197
2360
|
runs-on: ubuntu-latest
|
|
2198
2361
|
permissions:
|
|
2199
2362
|
contents: read
|
|
2363
|
+
pull-requests: read
|
|
2364
|
+
issues: read
|
|
2200
2365
|
steps:
|
|
2201
2366
|
- uses: actions/checkout@v4
|
|
2202
2367
|
with:
|
|
@@ -2205,36 +2370,94 @@ jobs:
|
|
|
2205
2370
|
with:
|
|
2206
2371
|
node-version: 20
|
|
2207
2372
|
- name: Run checks
|
|
2373
|
+
env:
|
|
2374
|
+
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2375
|
+
PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
2376
|
+
BRANCH: \${{ github.head_ref }}
|
|
2208
2377
|
run: npx --yes @attalabs/vinaya check --all --diff-only
|
|
2209
2378
|
`;
|
|
2210
2379
|
}
|
|
2211
2380
|
function reviewWorkflow() {
|
|
2212
2381
|
return `# ${MANAGED_NOTE}
|
|
2213
2382
|
#
|
|
2214
|
-
# The review gate —
|
|
2215
|
-
#
|
|
2216
|
-
#
|
|
2217
|
-
# structurally cannot receive
|
|
2218
|
-
#
|
|
2383
|
+
# The required review gate — pull_request events only. The verdict-comment
|
|
2384
|
+
# half lives in its own workflow (vinaya-review-verdict.yml): a new PR
|
|
2385
|
+
# comment fires a different GitHub event that this pull_request-only
|
|
2386
|
+
# workflow structurally cannot receive — and keeping the comment path in a
|
|
2387
|
+
# separate FILE means this workflow's runs never list permanently-skipped
|
|
2388
|
+
# comment jobs on the PR's checks panel. When a clean final verdict lands,
|
|
2389
|
+
# the verdict workflow re-runs this one, so the required check below goes
|
|
2390
|
+
# green natively with no manual rerun.
|
|
2219
2391
|
name: Vinaya Review Gate
|
|
2220
2392
|
|
|
2221
2393
|
on:
|
|
2222
2394
|
pull_request:
|
|
2223
2395
|
types: [opened, synchronize, reopened, labeled, unlabeled]
|
|
2224
|
-
issue_comment:
|
|
2225
|
-
types: [created]
|
|
2226
2396
|
|
|
2227
2397
|
jobs:
|
|
2228
2398
|
vinaya-review:
|
|
2229
2399
|
name: vinaya review gate
|
|
2230
|
-
if: >
|
|
2231
|
-
github.event_name == 'pull_request' ||
|
|
2232
|
-
(github.event.issue.pull_request != null && contains(github.event.comment.body, 'VERDICT'))
|
|
2233
2400
|
runs-on: ubuntu-latest
|
|
2234
2401
|
permissions:
|
|
2235
2402
|
contents: read
|
|
2236
2403
|
pull-requests: read
|
|
2237
2404
|
issues: read
|
|
2405
|
+
steps:
|
|
2406
|
+
- uses: actions/checkout@v4
|
|
2407
|
+
with:
|
|
2408
|
+
# This job executes repo content; don't leave the token in
|
|
2409
|
+
# .git/config for scripts to read.
|
|
2410
|
+
persist-credentials: false
|
|
2411
|
+
fetch-depth: 0
|
|
2412
|
+
- uses: actions/setup-node@v4
|
|
2413
|
+
with:
|
|
2414
|
+
node-version: 20
|
|
2415
|
+
- name: Review gate
|
|
2416
|
+
env:
|
|
2417
|
+
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2418
|
+
# PR_NUMBER is what makes the review-gate check EVALUATE: without
|
|
2419
|
+
# it the adapter reads "no PR yet — local dev" and exits 0, and
|
|
2420
|
+
# the gate is green regardless of review state.
|
|
2421
|
+
PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
2422
|
+
BRANCH: \${{ github.head_ref }}
|
|
2423
|
+
run: npx --yes @attalabs/vinaya check --all
|
|
2424
|
+
`;
|
|
2425
|
+
}
|
|
2426
|
+
function reviewVerdictWorkflow() {
|
|
2427
|
+
return `# ${MANAGED_NOTE}
|
|
2428
|
+
#
|
|
2429
|
+
# The verdict-comment half of the review gate. A reviewer's verdict arrives
|
|
2430
|
+
# as a PR comment (\`VERDICT: APPROVE\` / \`VERDICT: PASS\`), which fires
|
|
2431
|
+
# GitHub's \`issue_comment\` event — an event the required pull_request
|
|
2432
|
+
# workflow cannot receive. This workflow evaluates the gate on that comment
|
|
2433
|
+
# and, when the evaluation is clean, RE-RUNS the required workflow so its
|
|
2434
|
+
# check goes green natively with no manual rerun. (Writing check-run
|
|
2435
|
+
# conclusions directly is no longer possible: GitHub's 2025-02-12 change
|
|
2436
|
+
# restricts check-run updates to the owning workflow — re-running is the
|
|
2437
|
+
# supported channel.)
|
|
2438
|
+
#
|
|
2439
|
+
# Privilege split, deliberate: \`evaluate\` checks out and executes repo
|
|
2440
|
+
# content and therefore holds NO write permission; \`retrigger\` holds
|
|
2441
|
+
# \`actions: write\` but checks out and executes nothing — its only inputs
|
|
2442
|
+
# are the evaluator's outputs, resolved via \`gh pr view\` before any repo
|
|
2443
|
+
# content ran. A malicious branch can at worst fail its own evaluation.
|
|
2444
|
+
name: Vinaya Review Gate (on verdict)
|
|
2445
|
+
|
|
2446
|
+
on:
|
|
2447
|
+
issue_comment:
|
|
2448
|
+
types: [created]
|
|
2449
|
+
|
|
2450
|
+
jobs:
|
|
2451
|
+
evaluate:
|
|
2452
|
+
name: vinaya review gate (verdict check)
|
|
2453
|
+
if: github.event.issue.pull_request != null && contains(github.event.comment.body, 'VERDICT')
|
|
2454
|
+
runs-on: ubuntu-latest
|
|
2455
|
+
permissions:
|
|
2456
|
+
contents: read
|
|
2457
|
+
pull-requests: read
|
|
2458
|
+
issues: read
|
|
2459
|
+
outputs:
|
|
2460
|
+
branch: \${{ steps.pr.outputs.branch }}
|
|
2238
2461
|
steps:
|
|
2239
2462
|
# issue_comment payloads carry no PR head SHA/branch — resolve them
|
|
2240
2463
|
# before checkout, and check out that exact commit (the event's default
|
|
@@ -2244,24 +2467,54 @@ jobs:
|
|
|
2244
2467
|
env:
|
|
2245
2468
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2246
2469
|
run: |
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
else
|
|
2250
|
-
NUMBER="\${{ github.event.issue.number }}"
|
|
2251
|
-
fi
|
|
2470
|
+
NUMBER="\${{ github.event.issue.number }}"
|
|
2471
|
+
BRANCH=$(gh pr view "$NUMBER" --repo "\${{ github.repository }}" --json headRefName -q .headRefName)
|
|
2252
2472
|
SHA=$(gh pr view "$NUMBER" --repo "\${{ github.repository }}" --json headRefOid -q .headRefOid)
|
|
2473
|
+
echo "number=$NUMBER" >> "$GITHUB_OUTPUT"
|
|
2474
|
+
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
|
|
2253
2475
|
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
|
|
2254
2476
|
- uses: actions/checkout@v4
|
|
2255
2477
|
with:
|
|
2256
2478
|
ref: \${{ steps.pr.outputs.sha }}
|
|
2479
|
+
persist-credentials: false
|
|
2257
2480
|
fetch-depth: 0
|
|
2258
2481
|
- uses: actions/setup-node@v4
|
|
2259
2482
|
with:
|
|
2260
2483
|
node-version: 20
|
|
2261
|
-
- name: Review gate
|
|
2484
|
+
- name: Review gate (verdict evaluation)
|
|
2262
2485
|
env:
|
|
2263
2486
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2264
|
-
|
|
2487
|
+
# Same wiring as the required workflow: PR_NUMBER is what makes
|
|
2488
|
+
# the adapter evaluate instead of no-op'ing as "local dev".
|
|
2489
|
+
PR_NUMBER: \${{ steps.pr.outputs.number }}
|
|
2490
|
+
BRANCH: \${{ steps.pr.outputs.branch }}
|
|
2491
|
+
run: npx --yes @attalabs/vinaya check review-gate
|
|
2492
|
+
|
|
2493
|
+
# Executes nothing; consumes only the evaluator's outputs. Fires only on a
|
|
2494
|
+
# clean evaluation — a failed one leaves the standing red untouched.
|
|
2495
|
+
retrigger:
|
|
2496
|
+
name: vinaya review gate (retrigger)
|
|
2497
|
+
if: needs.evaluate.result == 'success'
|
|
2498
|
+
needs: evaluate
|
|
2499
|
+
runs-on: ubuntu-latest
|
|
2500
|
+
permissions:
|
|
2501
|
+
actions: write
|
|
2502
|
+
steps:
|
|
2503
|
+
- name: Re-run the required review gate for this branch
|
|
2504
|
+
env:
|
|
2505
|
+
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2506
|
+
BRANCH: \${{ needs.evaluate.outputs.branch }}
|
|
2507
|
+
run: |
|
|
2508
|
+
RUN_ID=$(gh run list --repo "\${{ github.repository }}" \\
|
|
2509
|
+
--workflow vinaya-review.yml --branch "$BRANCH" \\
|
|
2510
|
+
--status completed \\
|
|
2511
|
+
--json databaseId,event \\
|
|
2512
|
+
--jq '[.[] | select(.event=="pull_request")][0].databaseId // empty')
|
|
2513
|
+
if [ -z "$RUN_ID" ]; then
|
|
2514
|
+
echo "No completed pull_request run of vinaya-review.yml for branch $BRANCH - nothing to re-run."
|
|
2515
|
+
exit 0
|
|
2516
|
+
fi
|
|
2517
|
+
gh run rerun "$RUN_ID" --repo "\${{ github.repository }}"
|
|
2265
2518
|
`;
|
|
2266
2519
|
}
|
|
2267
2520
|
function archivistWorkflow() {
|
|
@@ -2345,14 +2598,18 @@ jobs:
|
|
|
2345
2598
|
}
|
|
2346
2599
|
var HOOK_PREAMBLE = `#!/usr/bin/env sh
|
|
2347
2600
|
`;
|
|
2601
|
+
function ownVersion() {
|
|
2602
|
+
const pkg = JSON.parse(readFileSync3(join6(packageRoot(import.meta.url), "package.json"), "utf-8"));
|
|
2603
|
+
return pkg.version;
|
|
2604
|
+
}
|
|
2348
2605
|
function preCommitBody() {
|
|
2349
2606
|
return `# Vinaya commit-time gate. Runs the deterministic checks over your staged
|
|
2350
2607
|
# diff before the commit lands.
|
|
2351
|
-
npx --
|
|
2608
|
+
npx --yes @attalabs/vinaya@${ownVersion()} check --all --diff-only || exit 1`;
|
|
2352
2609
|
}
|
|
2353
2610
|
function prePushBody() {
|
|
2354
2611
|
return `# Vinaya pre-push gate. Runs branch/dispatch checks before the push leaves.
|
|
2355
|
-
npx --
|
|
2612
|
+
npx --yes @attalabs/vinaya@${ownVersion()} check --all || exit 1`;
|
|
2356
2613
|
}
|
|
2357
2614
|
function doctrinePointer() {
|
|
2358
2615
|
const doctrineRoot = join6(packageRoot(import.meta.url), "aeg-root");
|
|
@@ -2461,6 +2718,12 @@ function buildInitOps(ctx) {
|
|
|
2461
2718
|
const hookMode = 493;
|
|
2462
2719
|
ops.push({ kind: "create-file", path: CHECKS_WORKFLOW_PATH, content: checksWorkflow(), group: "CI workflows" });
|
|
2463
2720
|
ops.push({ kind: "create-file", path: REVIEW_WORKFLOW_PATH, content: reviewWorkflow(), group: "CI workflows" });
|
|
2721
|
+
ops.push({
|
|
2722
|
+
kind: "create-file",
|
|
2723
|
+
path: REVIEW_VERDICT_WORKFLOW_PATH,
|
|
2724
|
+
content: reviewVerdictWorkflow(),
|
|
2725
|
+
group: "CI workflows"
|
|
2726
|
+
});
|
|
2464
2727
|
ops.push({ kind: "create-file", path: ARCHIVIST_WORKFLOW_PATH, content: archivistWorkflow(), group: "CI workflows" });
|
|
2465
2728
|
ops.push({
|
|
2466
2729
|
kind: "managed-block",
|
|
@@ -2519,7 +2782,7 @@ function buildInitProductOps(name) {
|
|
|
2519
2782
|
}
|
|
2520
2783
|
|
|
2521
2784
|
// src/lib/env-lint.ts
|
|
2522
|
-
import { existsSync as existsSync6, readFileSync as
|
|
2785
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
2523
2786
|
var ENV_READ_PATTERN = /\b(?:process\.env|Bun\.env|Deno\.env)\b/;
|
|
2524
2787
|
function checksMissingEnvDeclaration(specs) {
|
|
2525
2788
|
const names = [];
|
|
@@ -2530,7 +2793,7 @@ function checksMissingEnvDeclaration(specs) {
|
|
|
2530
2793
|
continue;
|
|
2531
2794
|
let source;
|
|
2532
2795
|
try {
|
|
2533
|
-
source =
|
|
2796
|
+
source = readFileSync4(spec.run, "utf8");
|
|
2534
2797
|
} catch {
|
|
2535
2798
|
continue;
|
|
2536
2799
|
}
|
|
@@ -2544,7 +2807,7 @@ function envDeclarationWarning(name) {
|
|
|
2544
2807
|
}
|
|
2545
2808
|
|
|
2546
2809
|
// src/lib/ops.ts
|
|
2547
|
-
import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as
|
|
2810
|
+
import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2548
2811
|
import { dirname as dirname3, join as join7, resolve, sep } from "node:path";
|
|
2549
2812
|
var MARKER_NS = "vinaya:managed";
|
|
2550
2813
|
function markerLines(marker, comment) {
|
|
@@ -2574,7 +2837,7 @@ function fileContains(repoRoot, relPath, needle) {
|
|
|
2574
2837
|
const p = abs(repoRoot, relPath);
|
|
2575
2838
|
if (!existsSync7(p))
|
|
2576
2839
|
return false;
|
|
2577
|
-
return
|
|
2840
|
+
return readFileSync5(p, "utf-8").includes(needle);
|
|
2578
2841
|
}
|
|
2579
2842
|
function planInstall(ops, repoRoot, ownedFiles = new Set) {
|
|
2580
2843
|
const entries = [];
|
|
@@ -2684,7 +2947,7 @@ function writeFileWithDirs(target, content, mode) {
|
|
|
2684
2947
|
}
|
|
2685
2948
|
function appendBlock(repoRoot, op) {
|
|
2686
2949
|
const target = abs(repoRoot, op.path);
|
|
2687
|
-
const existing =
|
|
2950
|
+
const existing = readFileSync5(target, "utf-8");
|
|
2688
2951
|
const sep2 = existing.endsWith(`
|
|
2689
2952
|
`) ? `
|
|
2690
2953
|
` : `
|
|
@@ -2806,7 +3069,7 @@ function planEject(manifest, repoRoot) {
|
|
|
2806
3069
|
});
|
|
2807
3070
|
continue;
|
|
2808
3071
|
}
|
|
2809
|
-
const content =
|
|
3072
|
+
const content = readFileSync5(p, "utf-8");
|
|
2810
3073
|
const stripped = stripBlockFromContent(content, b.marker, b.comment);
|
|
2811
3074
|
const removesHost = stripped !== null && blockStripLeavesEmpty(stripped);
|
|
2812
3075
|
actions.push({
|
|
@@ -2858,7 +3121,7 @@ function applyEject(plan, repoRoot) {
|
|
|
2858
3121
|
const p = containedAbs(repoRoot, a.path);
|
|
2859
3122
|
if (p === null)
|
|
2860
3123
|
continue;
|
|
2861
|
-
const content =
|
|
3124
|
+
const content = readFileSync5(p, "utf-8");
|
|
2862
3125
|
const stripped = stripBlockFromContent(content, a.marker, a.comment);
|
|
2863
3126
|
if (stripped === null)
|
|
2864
3127
|
continue;
|
|
@@ -2881,7 +3144,7 @@ function applyEject(plan, repoRoot) {
|
|
|
2881
3144
|
|
|
2882
3145
|
// src/commands/doctor.ts
|
|
2883
3146
|
function readVersion() {
|
|
2884
|
-
const pkg = JSON.parse(
|
|
3147
|
+
const pkg = JSON.parse(readFileSync6(join8(packageRoot(import.meta.url), "package.json"), "utf-8"));
|
|
2885
3148
|
return pkg.version;
|
|
2886
3149
|
}
|
|
2887
3150
|
function realDeps3() {
|
|
@@ -2905,7 +3168,7 @@ function readConfig(repoRoot) {
|
|
|
2905
3168
|
return { kind: "missing" };
|
|
2906
3169
|
let raw;
|
|
2907
3170
|
try {
|
|
2908
|
-
raw = JSON.parse(
|
|
3171
|
+
raw = JSON.parse(readFileSync6(p, "utf-8"));
|
|
2909
3172
|
} catch (err) {
|
|
2910
3173
|
return { kind: "invalid", error: `invalid JSON: ${err.message}` };
|
|
2911
3174
|
}
|
|
@@ -2941,7 +3204,7 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
|
|
|
2941
3204
|
findings.push(owned ? error(check, `${op.path} is recorded as vinaya-managed but missing on disk — run \`vinaya upgrade\`.`) : error(check, `${op.path} is not installed — run \`vinaya init\`.`));
|
|
2942
3205
|
continue;
|
|
2943
3206
|
}
|
|
2944
|
-
const content =
|
|
3207
|
+
const content = readFileSync6(abs2, "utf-8");
|
|
2945
3208
|
if (!owned) {
|
|
2946
3209
|
findings.push(content === op.content ? warn(check, `${op.path} has vinaya's own content but isn't recorded in the manifest.`) : info(check, `${op.path} exists but is foreign content — not vinaya-managed, left untouched.`));
|
|
2947
3210
|
continue;
|
|
@@ -2964,7 +3227,7 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
|
|
|
2964
3227
|
findings.push(owned ? error(check, `${op.path} is missing — likely a fresh clone (raw git hooks aren't tracked by git). Run \`vinaya upgrade\` to restore it.`) : info(check, `${op.path} is not installed.`));
|
|
2965
3228
|
continue;
|
|
2966
3229
|
}
|
|
2967
|
-
const content =
|
|
3230
|
+
const content = readFileSync6(abs2, "utf-8");
|
|
2968
3231
|
const { begin, end } = markerLines(op.marker, op.comment);
|
|
2969
3232
|
const hasMarkers = content.includes(begin) && content.includes(end);
|
|
2970
3233
|
if (!hasMarkers) {
|
|
@@ -3007,6 +3270,34 @@ function diagnoseEnvDeclarations(repoRoot, config) {
|
|
|
3007
3270
|
}
|
|
3008
3271
|
return findings;
|
|
3009
3272
|
}
|
|
3273
|
+
function diagnoseCheckClassification(config) {
|
|
3274
|
+
const classification = resolveChecks(coreCheckRegistry(), config?.checks);
|
|
3275
|
+
const findings = [];
|
|
3276
|
+
for (const entry2 of classification.resolved) {
|
|
3277
|
+
if (entry2.state === "overridden")
|
|
3278
|
+
findings.push(warn("checks", overriddenNextMinorWarning(entry2.name)));
|
|
3279
|
+
}
|
|
3280
|
+
for (const failure of classification.failures) {
|
|
3281
|
+
findings.push(warn("checks", bareKeyNextMinorWarning(failure.key)));
|
|
3282
|
+
}
|
|
3283
|
+
return findings;
|
|
3284
|
+
}
|
|
3285
|
+
function diagnoseGlobalConfigChecks() {
|
|
3286
|
+
if (!existsSync8(GLOBAL_CONFIG_PATH))
|
|
3287
|
+
return [];
|
|
3288
|
+
let raw;
|
|
3289
|
+
try {
|
|
3290
|
+
raw = JSON.parse(readFileSync6(GLOBAL_CONFIG_PATH, "utf-8"));
|
|
3291
|
+
} catch (err) {
|
|
3292
|
+
return [info("checks", `${GLOBAL_CONFIG_PATH} is invalid JSON — ${err.message}`)];
|
|
3293
|
+
}
|
|
3294
|
+
const parsed = VinayaConfigSchema.safeParse(raw);
|
|
3295
|
+
if (!parsed.success)
|
|
3296
|
+
return [];
|
|
3297
|
+
if (!parsed.data.checks || Object.keys(parsed.data.checks).length === 0)
|
|
3298
|
+
return [];
|
|
3299
|
+
return [warn("checks", globalChecksIgnoredWarning(GLOBAL_CONFIG_PATH))];
|
|
3300
|
+
}
|
|
3010
3301
|
async function diagnoseEnvironment(deps, hasDrift) {
|
|
3011
3302
|
const findings = [];
|
|
3012
3303
|
const auth = await deps.ghAuthStatus();
|
|
@@ -3077,6 +3368,8 @@ async function runDoctor(args, deps) {
|
|
|
3077
3368
|
findings.push(...diagnoseCustomChecks(repo.repoRoot, configRead.config));
|
|
3078
3369
|
}
|
|
3079
3370
|
findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
|
|
3371
|
+
findings.push(...diagnoseCheckClassification(configRead.kind === "ok" ? configRead.config : null));
|
|
3372
|
+
findings.push(...diagnoseGlobalConfigChecks());
|
|
3080
3373
|
findings.push(...await diagnoseEnvironment(deps, hasDrift));
|
|
3081
3374
|
findings.push(await diagnoseBranchProtection(deps, repo.owner, repo.repo));
|
|
3082
3375
|
const healthy = findings.every((f) => f.severity === "ok" || f.severity === "info");
|
|
@@ -3092,7 +3385,7 @@ async function doctorCommand(args) {
|
|
|
3092
3385
|
}
|
|
3093
3386
|
|
|
3094
3387
|
// src/commands/eject.ts
|
|
3095
|
-
import { existsSync as existsSync9, readFileSync as
|
|
3388
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
|
|
3096
3389
|
import { join as join9 } from "node:path";
|
|
3097
3390
|
|
|
3098
3391
|
// src/lib/prompt.ts
|
|
@@ -3179,7 +3472,7 @@ function readManifest(repoRoot) {
|
|
|
3179
3472
|
return { kind: "none" };
|
|
3180
3473
|
let raw;
|
|
3181
3474
|
try {
|
|
3182
|
-
raw = JSON.parse(
|
|
3475
|
+
raw = JSON.parse(readFileSync7(p, "utf-8"));
|
|
3183
3476
|
} catch (err) {
|
|
3184
3477
|
return { kind: "orphan", reason: `vinaya.config.json is not valid JSON: ${err.message}` };
|
|
3185
3478
|
}
|
|
@@ -3262,7 +3555,7 @@ async function ejectCommand(args) {
|
|
|
3262
3555
|
}
|
|
3263
3556
|
|
|
3264
3557
|
// src/commands/init.ts
|
|
3265
|
-
import { existsSync as existsSync10, readFileSync as
|
|
3558
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3266
3559
|
import { join as join10 } from "node:path";
|
|
3267
3560
|
function realDeps5() {
|
|
3268
3561
|
return {
|
|
@@ -3287,14 +3580,14 @@ function readManifest2(repoRoot) {
|
|
|
3287
3580
|
if (!existsSync10(p))
|
|
3288
3581
|
return null;
|
|
3289
3582
|
try {
|
|
3290
|
-
return VinayaConfigSchema.parse(JSON.parse(
|
|
3583
|
+
return VinayaConfigSchema.parse(JSON.parse(readFileSync8(p, "utf-8"))).managed ?? null;
|
|
3291
3584
|
} catch {
|
|
3292
3585
|
return null;
|
|
3293
3586
|
}
|
|
3294
3587
|
}
|
|
3295
3588
|
function writeManifest(repoRoot, manifest) {
|
|
3296
3589
|
const configAbs = join10(repoRoot, CONFIG_PATH);
|
|
3297
|
-
const seed = JSON.parse(
|
|
3590
|
+
const seed = JSON.parse(readFileSync8(configAbs, "utf-8"));
|
|
3298
3591
|
writeFileSync4(configAbs, `${JSON.stringify({ ...seed, managed: manifest }, null, 2)}
|
|
3299
3592
|
`, "utf-8");
|
|
3300
3593
|
}
|
|
@@ -3434,7 +3727,7 @@ import { execFileSync as execFileSync6 } from "node:child_process";
|
|
|
3434
3727
|
|
|
3435
3728
|
// src/lib/forge-write.ts
|
|
3436
3729
|
import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3437
|
-
import { readFileSync as
|
|
3730
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
3438
3731
|
import { tmpdir } from "node:os";
|
|
3439
3732
|
import { join as join11 } from "node:path";
|
|
3440
3733
|
class ForgeArgError extends Error {
|
|
@@ -3446,13 +3739,13 @@ function locateBody(args) {
|
|
|
3446
3739
|
const p = args[i + 1];
|
|
3447
3740
|
if (!p)
|
|
3448
3741
|
throw new ForgeArgError("`--body-file` was given with no path.");
|
|
3449
|
-
return { body:
|
|
3742
|
+
return { body: readFileSync9(p, "utf8"), source: { kind: "file", argIndex: i + 1, inlineForm: false } };
|
|
3450
3743
|
}
|
|
3451
3744
|
if (a.startsWith("--body-file=")) {
|
|
3452
3745
|
const p = a.slice("--body-file=".length);
|
|
3453
3746
|
if (!p)
|
|
3454
3747
|
throw new ForgeArgError("`--body-file=` was given with no path.");
|
|
3455
|
-
return { body:
|
|
3748
|
+
return { body: readFileSync9(p, "utf8"), source: { kind: "file", argIndex: i, inlineForm: true } };
|
|
3456
3749
|
}
|
|
3457
3750
|
if (a === "--body" || a === "-b") {
|
|
3458
3751
|
const v = args[i + 1];
|
|
@@ -3742,7 +4035,7 @@ function issueEditCommand(args) {
|
|
|
3742
4035
|
}
|
|
3743
4036
|
|
|
3744
4037
|
// src/commands/new-check.ts
|
|
3745
|
-
import { chmodSync as chmodSync2, existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as
|
|
4038
|
+
import { chmodSync as chmodSync2, existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3746
4039
|
import { join as join12 } from "node:path";
|
|
3747
4040
|
var TEMPLATE_PATH = join12(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
|
|
3748
4041
|
var CHECKS_DIR = join12("scripts", "vinaya-checks");
|
|
@@ -3761,7 +4054,7 @@ function newCheckCommand(args) {
|
|
|
3761
4054
|
console.error(`Error: ${targetPath} already exists.`);
|
|
3762
4055
|
process.exit(1);
|
|
3763
4056
|
}
|
|
3764
|
-
const template =
|
|
4057
|
+
const template = readFileSync10(TEMPLATE_PATH, "utf-8");
|
|
3765
4058
|
const contents = template.split("{{CHECK_NAME}}").join(name);
|
|
3766
4059
|
writeFileSync6(targetPath, contents, "utf-8");
|
|
3767
4060
|
chmodSync2(targetPath, 493);
|
|
@@ -3925,7 +4218,7 @@ function prEditCommand(args) {
|
|
|
3925
4218
|
|
|
3926
4219
|
// src/commands/studio.ts
|
|
3927
4220
|
import { spawn as spawn2 } from "node:child_process";
|
|
3928
|
-
import { existsSync as existsSync12, readFileSync as
|
|
4221
|
+
import { existsSync as existsSync12, readFileSync as readFileSync11 } from "node:fs";
|
|
3929
4222
|
import { dirname as dirname4, join as join13 } from "node:path";
|
|
3930
4223
|
function resolveStudioTarget(cwd) {
|
|
3931
4224
|
let dir = cwd;
|
|
@@ -3934,7 +4227,7 @@ function resolveStudioTarget(cwd) {
|
|
|
3934
4227
|
const pkgPath = join13(webDir, "package.json");
|
|
3935
4228
|
if (existsSync12(pkgPath)) {
|
|
3936
4229
|
try {
|
|
3937
|
-
const pkg = JSON.parse(
|
|
4230
|
+
const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
|
|
3938
4231
|
if (pkg.name === "@atta/vinaya-web") {
|
|
3939
4232
|
return { kind: "workspace", webDir };
|
|
3940
4233
|
}
|
|
@@ -3967,7 +4260,7 @@ async function runStudio(cwd, args) {
|
|
|
3967
4260
|
}
|
|
3968
4261
|
|
|
3969
4262
|
// src/commands/upgrade.ts
|
|
3970
|
-
import { existsSync as existsSync13, readFileSync as
|
|
4263
|
+
import { existsSync as existsSync13, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "node:fs";
|
|
3971
4264
|
import { join as join14 } from "node:path";
|
|
3972
4265
|
function realDeps6() {
|
|
3973
4266
|
return {
|
|
@@ -3989,7 +4282,7 @@ function readManifest3(repoRoot) {
|
|
|
3989
4282
|
return { kind: "missing" };
|
|
3990
4283
|
let raw;
|
|
3991
4284
|
try {
|
|
3992
|
-
raw = JSON.parse(
|
|
4285
|
+
raw = JSON.parse(readFileSync12(p, "utf-8"));
|
|
3993
4286
|
} catch (err) {
|
|
3994
4287
|
return { kind: "invalid", error: `invalid JSON: ${err.message}` };
|
|
3995
4288
|
}
|
|
@@ -4006,7 +4299,7 @@ function readManifest3(repoRoot) {
|
|
|
4006
4299
|
}
|
|
4007
4300
|
function writeManifestVersion(repoRoot, manifest) {
|
|
4008
4301
|
const configAbs = join14(repoRoot, CONFIG_PATH);
|
|
4009
|
-
const seed = JSON.parse(
|
|
4302
|
+
const seed = JSON.parse(readFileSync12(configAbs, "utf-8"));
|
|
4010
4303
|
const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
|
|
4011
4304
|
writeFileSync7(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
|
|
4012
4305
|
`, "utf-8");
|
|
@@ -4030,7 +4323,7 @@ function planUpgrade(ops, repoRoot, manifest) {
|
|
|
4030
4323
|
} else if (!exists) {
|
|
4031
4324
|
action = "recreate";
|
|
4032
4325
|
hasChanges = true;
|
|
4033
|
-
} else if (
|
|
4326
|
+
} else if (readFileSync12(abs2, "utf-8") !== op.content) {
|
|
4034
4327
|
action = "regenerate";
|
|
4035
4328
|
hasChanges = true;
|
|
4036
4329
|
} else {
|
|
@@ -4047,7 +4340,7 @@ function planUpgrade(ops, repoRoot, manifest) {
|
|
|
4047
4340
|
action = "recreate-host";
|
|
4048
4341
|
hasChanges = true;
|
|
4049
4342
|
} else {
|
|
4050
|
-
const content =
|
|
4343
|
+
const content = readFileSync12(abs2, "utf-8");
|
|
4051
4344
|
const { begin, end } = markerLines(op.marker, op.comment);
|
|
4052
4345
|
if (!(content.includes(begin) && content.includes(end))) {
|
|
4053
4346
|
action = "recreate-append";
|
|
@@ -4124,7 +4417,7 @@ function renderUpgradeDiff(plan) {
|
|
|
4124
4417
|
}
|
|
4125
4418
|
function regenerateBlock(repoRoot, op) {
|
|
4126
4419
|
const abs2 = join14(repoRoot, op.path);
|
|
4127
|
-
const content =
|
|
4420
|
+
const content = readFileSync12(abs2, "utf-8");
|
|
4128
4421
|
const stripped = stripBlockFromContent(content, op.marker, op.comment);
|
|
4129
4422
|
if (stripped !== null) {
|
|
4130
4423
|
writeFileSync7(abs2, stripped.endsWith(`
|
|
@@ -4363,10 +4656,15 @@ var COMMANDS = [
|
|
|
4363
4656
|
{ flag: "--all", description: "Run every registered check instead of one named check" },
|
|
4364
4657
|
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" },
|
|
4365
4658
|
{ flag: "--diff-only", description: "Scope diff-declared checks to changed files" },
|
|
4366
|
-
{ flag: "--parallel[=n]", description: "Concurrency cap (default: cpu-derived)" }
|
|
4659
|
+
{ flag: "--parallel[=n]", description: "Concurrency cap (default: cpu-derived)" },
|
|
4660
|
+
{
|
|
4661
|
+
flag: "--plan",
|
|
4662
|
+
description: "Print the resolved check registry (default/overridden/additive) without running anything"
|
|
4663
|
+
}
|
|
4367
4664
|
],
|
|
4368
4665
|
details: [
|
|
4369
|
-
"Each spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its `CheckSpec['env']` declaration explicitly forwards — never the full parent environment. A required (`true`) or unsatisfied `anyOf` declaration missing from the caller's environment synthesizes a `CheckError` before the check ever spawns. Declare `env` (a core check's own registration, or `vinaya.config.json`'s `checks.<name>.env` for a custom one) for any check that reads `process.env`/`Bun.env`/`Deno.env` directly — `vinaya doctor` carries the permanent diagnostic for one that doesn't."
|
|
4666
|
+
"Each spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its `CheckSpec['env']` declaration explicitly forwards — never the full parent environment. A required (`true`) or unsatisfied `anyOf` declaration missing from the caller's environment synthesizes a `CheckError` before the check ever spawns. Declare `env` (a core check's own registration, or `vinaya.config.json`'s `checks.<name>.env` for a custom one) for any check that reads `process.env`/`Bun.env`/`Deno.env` directly — `vinaya doctor` carries the permanent diagnostic for one that doesn't.",
|
|
4667
|
+
"`--plan` composes with `--json`. It requires zero env vars and never prints an env value — only how each one resolves (passthrough, optional, literal, or anyOf). A `FAIL_CLOSED` entry (a bare key with no namespace matching no core check) always renders inline rather than being dropped, and exits non-zero. `--plan` previews what the next minor's execution will enforce — `vinaya check` itself still runs the flat, unvalidated registry this release."
|
|
4370
4668
|
],
|
|
4371
4669
|
status: "shipped"
|
|
4372
4670
|
},
|
|
@@ -4544,7 +4842,7 @@ function printHelp() {
|
|
|
4544
4842
|
// src/index.ts
|
|
4545
4843
|
var PACKAGE_ROOT = join15(dirname5(fileURLToPath2(import.meta.url)), "..");
|
|
4546
4844
|
function readVersion2() {
|
|
4547
|
-
const pkg = JSON.parse(
|
|
4845
|
+
const pkg = JSON.parse(readFileSync13(join15(PACKAGE_ROOT, "package.json"), "utf-8"));
|
|
4548
4846
|
return pkg.version;
|
|
4549
4847
|
}
|
|
4550
4848
|
var [, , command, ...args] = process.argv;
|