@attalabs/vinaya 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/aeg-root/contracts/archivist-tranche-archivist.md +2 -2
- package/aeg-root/contracts/brief-developer.md +1 -0
- package/aeg-root/contracts/developer-reviewer.md +3 -3
- package/aeg-root/contracts/reviewer-archivist.md +3 -3
- package/aeg-root/enforcement.md +26 -20
- package/aeg-root/process.md +8 -4
- package/aeg-root/roles/archivist.md +2 -2
- package/aeg-root/roles/brief-author.md +9 -3
- package/aeg-root/roles/developer.md +63 -31
- package/aeg-root/roles/planner.md +1 -1
- package/aeg-root/roles/principal.md +14 -0
- package/aeg-root/roles/reviewer.md +32 -15
- package/aeg-root/roles/security.md +15 -5
- package/aeg-root/skills/aeg/SKILL.md +9 -5
- package/aeg-root/skills/aeg-roles/SKILL.md +2 -2
- package/aeg-root/skills/brief-authoring/SKILL.md +39 -27
- package/aeg-root/state-machine.md +9 -11
- package/aeg-root/templates/brief-template.md +6 -6
- package/aeg-root/templates/pr-report-template.md +10 -7
- package/aeg-root/tranche-model.md +6 -2
- package/dist/checks/bin/check-body-bare-digits.js +552 -139
- package/dist/checks/bin/check-branch-topology.js +603 -144
- package/dist/checks/bin/check-brief-shape.js +593 -142
- package/dist/checks/bin/check-changeset-coverage.js +935 -161
- package/dist/checks/bin/check-closes-n.js +603 -144
- package/dist/checks/bin/check-coherence.js +614 -147
- package/dist/checks/bin/check-dead-branch-push.js +552 -139
- package/dist/checks/bin/check-dispatch-readiness.js +612 -145
- package/dist/checks/bin/check-doc-coverage-push.js +689 -218
- package/dist/checks/bin/check-doc-coverage.js +689 -218
- package/dist/checks/bin/check-doctrine-no-procedures.js +4794 -0
- package/dist/checks/bin/check-doctrine-portability.js +684 -146
- package/dist/checks/bin/check-evidence-fresh.js +994 -174
- package/dist/checks/bin/check-exec-bits.js +4931 -0
- package/dist/checks/bin/check-first-push-dispatch.js +603 -144
- package/dist/checks/bin/check-issue-assignment.js +603 -144
- package/dist/checks/bin/check-main-branch-refusal.js +552 -139
- package/dist/checks/bin/check-no-disk-state.js +552 -139
- package/dist/checks/bin/check-pr-report-density.js +4461 -0
- package/dist/checks/bin/check-quoted-command.js +621 -155
- package/dist/checks/bin/check-reader-resolvable-prose.js +617 -154
- package/dist/checks/bin/check-registry-gates.js +595 -144
- package/dist/checks/bin/check-retired-vocabulary.js +617 -154
- package/dist/checks/bin/check-review-gate.js +670 -141
- package/dist/checks/bin/check-single-plan-pr.js +552 -139
- package/dist/checks/bin/check-test-plan.js +553 -140
- package/dist/checks/bin/check-token-collection-wired.js +552 -139
- package/dist/checks/bin/check-token-report.js +552 -139
- package/dist/checks/bin/check-workspace-escape.js +985 -145
- package/dist/index.js +8500 -5518
- package/package.json +1 -1
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/actions.ts +173 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/anchored-region.ts +113 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/archive-task.ts +206 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/baseline-capture.ts +65 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/blast-radius-domains.ts +192 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/branch-topology-gate.ts +85 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-validation.ts +511 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/claude-code-transcript.ts +460 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/coherence-checks.ts +787 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-audit.ts +55 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-guard.ts +77 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-section7.ts +66 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-tranche.ts +94 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/diagram-model.ts +372 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/direct-main-push.ts +39 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dispatch-gate.ts +224 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doc-owners.ts +314 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/build-doc-nav.ts +38 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/docs-coherence.ts +136 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/index.ts +29 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/legacy-anchors.ts +48 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/nav-helpers.ts +17 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/node-route.ts +134 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/parse-doc.ts +43 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/published-prose.ts +306 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/surfaced-manifest.ts +89 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/types.ts +33 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-portability.ts +170 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-source.ts +23 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/ensure-label.ts +28 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/file-classify.ts +51 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/first-push-dispatch-gate.ts +80 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/gate-audience.ts +168 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +301 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-assignment.ts +102 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-validation.ts +746 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/leftover-detection.ts +68 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/local-anchor-coverage.ts +245 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +64 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/manifest-validity.ts +81 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/markdown-table.ts +76 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/metering-io-guard.ts +119 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/milestone-validation.ts +240 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/no-disk-state.ts +50 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-ledger.ts +77 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-registry.ts +105 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-token-report.ts +165 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-tranche.ts +183 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-tier.ts +81 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +125 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/quoted-command.ts +275 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/reader-resolvable-prose.ts +320 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-checks.ts +255 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-parse.ts +164 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-scaffold.ts +216 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/report-tokens.ts +261 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/retired-vocabulary.ts +163 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-gate.ts +269 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/single-plan-pr.ts +73 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-machine-model.ts +269 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-source.ts +14 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/status-block.ts +8 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/sum-ledger.ts +26 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/symbol-collisions.ts +78 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-gate.ts +104 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-section.ts +65 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/types.ts +174 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/verdict-extraction.ts +140 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/vocabulary-citation.ts +114 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/waiver-label.ts +52 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/workspace-escape.ts +172 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__02a-3g8._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/1q96_modules_@clerk_nextjs_dist_esm_app-router_client_keyless-creator-reader_0lom2js.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__069-_41._.js → [root-of-the-server]__1mcitst._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{37hnluhzj9bei.js → 0jer_drkl1e4r.js} +4 -4
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{267o-tsrnuwnv.js → 20x-t0a0v129z.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1hufpuody6vjv.js → 2ou_81ahqw31e.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{Fmyc42qzLwFcWWHLLNy-G → 5xy_GA7G5KRuE3HDumIiw}/_ssgManifest.js +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
2
4
|
|
|
3
5
|
// src/checks/bin/check-workspace-escape.ts
|
|
4
|
-
import { readdirSync, readFileSync as
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
+
import { readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
7
|
+
import { join as join3 } from "node:path";
|
|
6
8
|
// ../../packages/aeg-core/src/gate-audience.ts
|
|
7
9
|
var GATE_AUDIENCE = {
|
|
8
10
|
"check-branch-topology": { shippedAs: "branch-topology", ring: 0 },
|
|
@@ -211,17 +213,18 @@ function splitIds(raw) {
|
|
|
211
213
|
return [];
|
|
212
214
|
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0 && !isEmptyMarker(s) && ID_TOKEN.test(s));
|
|
213
215
|
}
|
|
214
|
-
function resolveIds(raw
|
|
216
|
+
function resolveIds(raw) {
|
|
215
217
|
const ids = splitIds(raw);
|
|
216
218
|
const resolved = [];
|
|
219
|
+
let lastSlug = null;
|
|
217
220
|
for (const id of ids) {
|
|
218
221
|
const qualified = id.match(SLUG_QUALIFIED_ID);
|
|
219
222
|
if (qualified) {
|
|
220
|
-
lastSlug
|
|
223
|
+
lastSlug = qualified[1] ?? null;
|
|
221
224
|
resolved.push(id);
|
|
222
|
-
} else if (lastSlug
|
|
223
|
-
resolved.push(`${lastSlug
|
|
224
|
-
lastSlug
|
|
225
|
+
} else if (lastSlug) {
|
|
226
|
+
resolved.push(`${lastSlug} ${id}`);
|
|
227
|
+
lastSlug = null;
|
|
225
228
|
} else {
|
|
226
229
|
resolved.push(id);
|
|
227
230
|
}
|
|
@@ -241,12 +244,7 @@ function parseRationaleDeps(body) {
|
|
|
241
244
|
const section = extractSection(body);
|
|
242
245
|
const dependsOn = [];
|
|
243
246
|
const conflictsWith = [];
|
|
244
|
-
let current = null;
|
|
245
247
|
const assignedFields = new Set;
|
|
246
|
-
const lastSlugByField = {
|
|
247
|
-
dependsOn: { current: null },
|
|
248
|
-
conflictsWith: { current: null }
|
|
249
|
-
};
|
|
250
248
|
const spanPattern = /`([^`]*)`/g;
|
|
251
249
|
let match = spanPattern.exec(section);
|
|
252
250
|
while (match !== null) {
|
|
@@ -256,13 +254,9 @@ function parseRationaleDeps(body) {
|
|
|
256
254
|
const field = labelMatch[1]?.toLowerCase() === "conflicts-with" ? "conflictsWith" : "dependsOn";
|
|
257
255
|
if (!assignedFields.has(field)) {
|
|
258
256
|
assignedFields.add(field);
|
|
259
|
-
|
|
260
|
-
const ids = resolveIds(labelMatch[2] ?? "", lastSlugByField[field]);
|
|
257
|
+
const ids = resolveIds(labelMatch[2] ?? "");
|
|
261
258
|
pushUnique(field === "dependsOn" ? dependsOn : conflictsWith, ids);
|
|
262
259
|
}
|
|
263
|
-
} else if (current) {
|
|
264
|
-
const ids = resolveIds(content, lastSlugByField[current]);
|
|
265
|
-
pushUnique(current === "dependsOn" ? dependsOn : conflictsWith, ids);
|
|
266
260
|
}
|
|
267
261
|
match = spanPattern.exec(section);
|
|
268
262
|
}
|
|
@@ -1727,6 +1721,48 @@ function splitTableRow(row) {
|
|
|
1727
1721
|
parts.pop();
|
|
1728
1722
|
return parts.map((p) => p.split(PLACEHOLDER).join("|"));
|
|
1729
1723
|
}
|
|
1724
|
+
// ../../packages/aeg-core/src/blast-radius-domains.ts
|
|
1725
|
+
function resolveWorkspaceEntry(entry2, listDirs) {
|
|
1726
|
+
if (entry2.endsWith("/*")) {
|
|
1727
|
+
const baseDir = entry2.slice(0, -2);
|
|
1728
|
+
return listDirs(baseDir).map((name) => `${baseDir}/${name}`);
|
|
1729
|
+
}
|
|
1730
|
+
if (entry2.includes("*"))
|
|
1731
|
+
return [];
|
|
1732
|
+
return [entry2];
|
|
1733
|
+
}
|
|
1734
|
+
// ../../packages/aeg-core/src/consumer-enumeration.ts
|
|
1735
|
+
function deriveWorkspaceMemberDirs(workspaces, listDirs) {
|
|
1736
|
+
const positive = workspaces.filter((entry2) => !entry2.startsWith("!"));
|
|
1737
|
+
const negative = workspaces.filter((entry2) => entry2.startsWith("!")).map((entry2) => entry2.slice(1));
|
|
1738
|
+
const excluded = new Set;
|
|
1739
|
+
for (const entry2 of negative) {
|
|
1740
|
+
for (const member of resolveWorkspaceEntry(entry2, listDirs))
|
|
1741
|
+
excluded.add(member);
|
|
1742
|
+
}
|
|
1743
|
+
const members = new Set;
|
|
1744
|
+
for (const entry2 of positive) {
|
|
1745
|
+
for (const member of resolveWorkspaceEntry(entry2, listDirs)) {
|
|
1746
|
+
if (!excluded.has(member))
|
|
1747
|
+
members.add(member);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
return [...members].sort();
|
|
1751
|
+
}
|
|
1752
|
+
function buildConsumersOf(workspaces, listDirs, readManifest) {
|
|
1753
|
+
const members = deriveWorkspaceMemberDirs(workspaces, listDirs);
|
|
1754
|
+
return (pkg) => {
|
|
1755
|
+
const target = `@attalabs/${pkg}`;
|
|
1756
|
+
return members.filter((dir) => {
|
|
1757
|
+
if (dir === `packages/${pkg}`)
|
|
1758
|
+
return false;
|
|
1759
|
+
const manifest = readManifest(dir);
|
|
1760
|
+
if (!manifest)
|
|
1761
|
+
return false;
|
|
1762
|
+
return target in (manifest.dependencies ?? {}) || target in (manifest.devDependencies ?? {});
|
|
1763
|
+
});
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1730
1766
|
// ../../packages/aeg-core/src/state-machine-model.ts
|
|
1731
1767
|
var FORGE_FACT_INPUTS = [
|
|
1732
1768
|
{
|
|
@@ -1831,6 +1867,22 @@ function isEmDashOrDash(s) {
|
|
|
1831
1867
|
const t = s.trim();
|
|
1832
1868
|
return t === "—" || t === "-" || t === "–";
|
|
1833
1869
|
}
|
|
1870
|
+
// ../../packages/aeg-core/src/waiver-label.ts
|
|
1871
|
+
var WAIVER_LABEL = label("waiver-docs");
|
|
1872
|
+
var WAIVER_LABEL_REVIEW = label("waiver-review");
|
|
1873
|
+
var PRINCIPAL_ALLOWLIST = ["daniboomerang"];
|
|
1874
|
+
function isPrincipal(login, principalAllowlist) {
|
|
1875
|
+
if (login === null)
|
|
1876
|
+
return false;
|
|
1877
|
+
const normalized = login.toLowerCase();
|
|
1878
|
+
return principalAllowlist.some((p) => p.toLowerCase() === normalized);
|
|
1879
|
+
}
|
|
1880
|
+
function isWaiverLabelActorVerified(opts) {
|
|
1881
|
+
if (!opts.labels.includes(opts.label))
|
|
1882
|
+
return false;
|
|
1883
|
+
return isPrincipal(opts.labelActor, opts.principalAllowlist);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1834
1886
|
// ../../packages/aeg-core/src/parse-token-report.ts
|
|
1835
1887
|
var TOKEN_REPORT_HEADING = /^\s*(?:#{1,6}\s*Token report\s*|\*\*Token report\*\*)\s*$/i;
|
|
1836
1888
|
var INLINE_FIELD_LIST = /Phase:\s*([^|]+?)\s*\|\s*Role:\s*([^|]+?)\s*\|\s*Agent\/Model:\s*([^|]+?)\s*\|\s*Tokens in:\s*([^|]+?)\s*\|\s*Tokens out:\s*([^|]+?)\s*\|\s*Cost:\s*([^|]+?)\s*\|\s*Date:\s*(.+?)\s*$/im;
|
|
@@ -1885,6 +1937,47 @@ function parseInlineFieldList(section) {
|
|
|
1885
1937
|
return null;
|
|
1886
1938
|
return rowFromCells([m[1], m[2], m[3], m[4], m[5], m[6], m[7]]);
|
|
1887
1939
|
}
|
|
1940
|
+
// ../../packages/aeg-core/src/verdict-extraction.ts
|
|
1941
|
+
var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
|
|
1942
|
+
function firstThreeLines(comment) {
|
|
1943
|
+
return comment.split(`
|
|
1944
|
+
`).slice(0, 3).join(`
|
|
1945
|
+
`);
|
|
1946
|
+
}
|
|
1947
|
+
function extractHeadSha(comment) {
|
|
1948
|
+
const m = firstThreeLines(comment).match(HEAD_SHA_PATTERN);
|
|
1949
|
+
return m ? m[1].toLowerCase() : null;
|
|
1950
|
+
}
|
|
1951
|
+
function extractVerdict(comments, valuePattern, missingLabel) {
|
|
1952
|
+
const candidates = comments.filter((c) => valuePattern.test(c));
|
|
1953
|
+
if (candidates.length === 0) {
|
|
1954
|
+
return {
|
|
1955
|
+
value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
|
|
1956
|
+
headSha: null,
|
|
1957
|
+
danglingNote: `no ${missingLabel} verdict comment found on this PR`
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
const latest = candidates[candidates.length - 1];
|
|
1961
|
+
const m = firstThreeLines(latest).match(valuePattern);
|
|
1962
|
+
if (!m) {
|
|
1963
|
+
return {
|
|
1964
|
+
value: `the most recent ${missingLabel} comment's VERDICT line is not within its first three lines — DANGLING, see below`,
|
|
1965
|
+
headSha: null,
|
|
1966
|
+
danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-three-line read window`
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
return {
|
|
1970
|
+
value: m[1].toUpperCase().replace(/[_-]/g, " "),
|
|
1971
|
+
headSha: extractHeadSha(latest),
|
|
1972
|
+
danglingNote: null
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
function extractCodeReviewVerdict(comments) {
|
|
1976
|
+
return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(APPROVE|REQUEST[ _-]?CHANGES|LGTM)(?![A-Za-z0-9])/im, "code-reviewer");
|
|
1977
|
+
}
|
|
1978
|
+
function extractSecurityReviewVerdict(comments) {
|
|
1979
|
+
return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(PASS|FAIL)(?![A-Za-z0-9])/im, "security-review");
|
|
1980
|
+
}
|
|
1888
1981
|
// ../../packages/aeg-core/src/report-tokens.ts
|
|
1889
1982
|
var DASH_LOOKALIKES = {
|
|
1890
1983
|
"-": "‑",
|
|
@@ -2049,22 +2142,6 @@ function isCodeFile(p) {
|
|
|
2049
2142
|
return false;
|
|
2050
2143
|
return /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|sql|css)$/.test(p);
|
|
2051
2144
|
}
|
|
2052
|
-
// ../../packages/aeg-core/src/waiver-label.ts
|
|
2053
|
-
var WAIVER_LABEL = label("waiver-docs");
|
|
2054
|
-
var WAIVER_LABEL_REVIEW = label("waiver-review");
|
|
2055
|
-
var PRINCIPAL_ALLOWLIST = ["daniboomerang"];
|
|
2056
|
-
function isPrincipal(login, principalAllowlist) {
|
|
2057
|
-
if (login === null)
|
|
2058
|
-
return false;
|
|
2059
|
-
const normalized = login.toLowerCase();
|
|
2060
|
-
return principalAllowlist.some((p) => p.toLowerCase() === normalized);
|
|
2061
|
-
}
|
|
2062
|
-
function isWaiverLabelActorVerified(opts) {
|
|
2063
|
-
if (!opts.labels.includes(opts.label))
|
|
2064
|
-
return false;
|
|
2065
|
-
return isPrincipal(opts.labelActor, opts.principalAllowlist);
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
2145
|
// ../../packages/aeg-core/src/doc-owners.ts
|
|
2069
2146
|
var DOC_OWNERS_PATH = ".vinaya/doc-owners";
|
|
2070
2147
|
function parseDocOwners(content) {
|
|
@@ -2281,6 +2358,32 @@ function parsePremiseBlock(prBody) {
|
|
|
2281
2358
|
}
|
|
2282
2359
|
return assertions;
|
|
2283
2360
|
}
|
|
2361
|
+
function premiseBlockText(prBody) {
|
|
2362
|
+
const searchIn = anchoredRegion(prBody, "PREMISE") ?? prBody;
|
|
2363
|
+
const lines = searchIn.split(/\r?\n/);
|
|
2364
|
+
let startIdx = -1;
|
|
2365
|
+
let endIdxExclusive = lines.length;
|
|
2366
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2367
|
+
const line = lines[i].trim();
|
|
2368
|
+
if (startIdx === -1) {
|
|
2369
|
+
if (isPremiseHeader(line))
|
|
2370
|
+
startIdx = i;
|
|
2371
|
+
continue;
|
|
2372
|
+
}
|
|
2373
|
+
if (line === "") {
|
|
2374
|
+
endIdxExclusive = i;
|
|
2375
|
+
break;
|
|
2376
|
+
}
|
|
2377
|
+
if (!PREMISE_LINE.test(line)) {
|
|
2378
|
+
endIdxExclusive = i;
|
|
2379
|
+
break;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
if (startIdx === -1)
|
|
2383
|
+
return null;
|
|
2384
|
+
return lines.slice(startIdx, endIdxExclusive).join(`
|
|
2385
|
+
`);
|
|
2386
|
+
}
|
|
2284
2387
|
function checkPremises(assertions, fileReader) {
|
|
2285
2388
|
const failures = [];
|
|
2286
2389
|
for (const a of assertions) {
|
|
@@ -2367,15 +2470,18 @@ function testPlanRegion(prBody) {
|
|
|
2367
2470
|
return located.found ? located.section : prBody;
|
|
2368
2471
|
}
|
|
2369
2472
|
function checkTestPlan(prBody) {
|
|
2370
|
-
const
|
|
2473
|
+
const located = locateTestPlanSection(prBody);
|
|
2474
|
+
const region = located.found ? located.section : prBody;
|
|
2371
2475
|
if (TEST_PLAN_UNIT_TESTS_ONLY_RE.test(region))
|
|
2372
2476
|
return { status: "pass", errors: [] };
|
|
2373
2477
|
if (/\*\*\[(?:agent|principal)\]\*\*/.test(region))
|
|
2374
2478
|
return { status: "pass", errors: [] };
|
|
2479
|
+
if (located.found && extractFencedBlocks(region).length > 0)
|
|
2480
|
+
return { status: "pass", errors: [] };
|
|
2375
2481
|
return {
|
|
2376
2482
|
status: "fail",
|
|
2377
2483
|
errors: [
|
|
2378
|
-
"brief-validation Test Plan: no Test Plan section found — expected `Test Plan: unit-tests-only`, or at least one `**[agent]**`/`**[principal]**`-tagged checklist item."
|
|
2484
|
+
"brief-validation Test Plan: no Test Plan section found — expected `Test Plan: unit-tests-only`, a fenced `[agent]` command list, or at least one `**[agent]**`/`**[principal]**`-tagged checklist item."
|
|
2379
2485
|
]
|
|
2380
2486
|
};
|
|
2381
2487
|
}
|
|
@@ -2498,8 +2604,162 @@ function isBriefShaped(prBody) {
|
|
|
2498
2604
|
const stripped = stripCode(prBody);
|
|
2499
2605
|
return BRIEF_SHAPE_MARKERS.filter((check) => check(stripped).status === "pass").length >= 2;
|
|
2500
2606
|
}
|
|
2607
|
+
var COMMAND_WORDS = ["export", "bun", "gh", "git", "grep", "sed", "cat", "diff", "vinaya"];
|
|
2608
|
+
var BRIEF_RULES_SINCE_PR = 394;
|
|
2609
|
+
var AGENT_BOXES_REFUSED_SINCE_PR = 396;
|
|
2610
|
+
var AGENT_BOX_LINE_RE = /^-\s*\[[ xX]\]\s*\*{2}\[agent\]\*{2}/im;
|
|
2611
|
+
function checkNoAgentBoxes(prBody) {
|
|
2612
|
+
const region = testPlanRegion(prBody);
|
|
2613
|
+
if (!AGENT_BOX_LINE_RE.test(region))
|
|
2614
|
+
return { status: "pass", errors: [] };
|
|
2615
|
+
return {
|
|
2616
|
+
status: "fail",
|
|
2617
|
+
errors: [
|
|
2618
|
+
"brief-validation no agent boxes: the Test Plan carries a checkbox `[agent]` item — an agent never ticks a box or edits a PR body (Principal ruling, 2026-09-03, after PR #395). Render the `[agent]` half as a fenced command list instead; `vinaya pr report` runs each command from the PR head and writes its actual output into the AEG:EVIDENCE block. `[principal]` checkboxes are unaffected."
|
|
2619
|
+
]
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
function rolloutThresholdFor(message) {
|
|
2623
|
+
if (message.startsWith("brief-validation unpinned code claim:") || message.startsWith("brief-validation commands carry output:") || message.startsWith("brief-validation consumer tests:") || message.startsWith("brief-validation defeat cases:")) {
|
|
2624
|
+
return BRIEF_RULES_SINCE_PR;
|
|
2625
|
+
}
|
|
2626
|
+
if (message.startsWith("brief-validation no agent boxes:"))
|
|
2627
|
+
return AGENT_BOXES_REFUSED_SINCE_PR;
|
|
2628
|
+
return null;
|
|
2629
|
+
}
|
|
2630
|
+
function partitionBriefErrorsByRollout(errors, prNumber) {
|
|
2631
|
+
const blocking = [];
|
|
2632
|
+
const info = [];
|
|
2633
|
+
for (const e of errors) {
|
|
2634
|
+
const threshold = rolloutThresholdFor(e);
|
|
2635
|
+
const grandfathered = threshold !== null && prNumber !== null && prNumber < threshold;
|
|
2636
|
+
(grandfathered ? info : blocking).push(e);
|
|
2637
|
+
}
|
|
2638
|
+
return { blocking, info };
|
|
2639
|
+
}
|
|
2640
|
+
function extractFencedBlocks(text) {
|
|
2641
|
+
const re = /^[ \t]*(`{3,}|~{3,})([^\n]*)\n([\s\S]*?)^[ \t]*\1[ \t]*$/gm;
|
|
2642
|
+
const blocks = [];
|
|
2643
|
+
let m = re.exec(text);
|
|
2644
|
+
while (m !== null) {
|
|
2645
|
+
const lang = m[2].trim().toLowerCase().split(/\s+/)[0] ?? "";
|
|
2646
|
+
blocks.push({ start: m.index, end: m.index + m[0].length, lang, content: m[3] });
|
|
2647
|
+
m = re.exec(text);
|
|
2648
|
+
}
|
|
2649
|
+
return blocks;
|
|
2650
|
+
}
|
|
2651
|
+
function firstNonBlankLine(text) {
|
|
2652
|
+
const line = text.split(`
|
|
2653
|
+
`).find((l) => l.trim().length > 0);
|
|
2654
|
+
return line?.trim() ?? "";
|
|
2655
|
+
}
|
|
2656
|
+
var FILE_LINE_RE = /\b[\w./-]+\.(?:tsx?|jsx?|mjs|cjs|md|mdx|json|ya?ml|sh|py|toml):\d+\b/g;
|
|
2657
|
+
function checkNoUnpinnedCodeClaims(prBody) {
|
|
2658
|
+
const premiseText = premiseBlockText(prBody);
|
|
2659
|
+
const withoutPremise = premiseText ? prBody.replace(premiseText, "") : prBody;
|
|
2660
|
+
const scanned = stripCode(withoutPremise, { inlineSpans: "keep" });
|
|
2661
|
+
const matches = scanned.match(FILE_LINE_RE) ?? [];
|
|
2662
|
+
if (matches.length === 0)
|
|
2663
|
+
return { status: "pass", errors: [] };
|
|
2664
|
+
return {
|
|
2665
|
+
status: "fail",
|
|
2666
|
+
errors: matches.map((m) => `brief-validation unpinned code claim: "${m}" states a code fact by file-and-line reference outside a \`Premise:\` pin and outside a fenced code block — pin it in \`Premise:\`, or show the executed command and its output instead.`)
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
function extractNumberedSection(prBody, num) {
|
|
2670
|
+
const startRe = new RegExp(`^#{1,4}\\s*(?:\\*\\*)?${num}[a-z]?\\.\\s`, "im");
|
|
2671
|
+
const start = startRe.exec(prBody);
|
|
2672
|
+
if (!start)
|
|
2673
|
+
return null;
|
|
2674
|
+
const afterStart = start.index + start[0].length;
|
|
2675
|
+
const rest = prBody.slice(afterStart);
|
|
2676
|
+
const next = /^#{1,4}\s/m.exec(rest);
|
|
2677
|
+
return next ? rest.slice(0, next.index) : rest;
|
|
2678
|
+
}
|
|
2679
|
+
function commandWordOf(line) {
|
|
2680
|
+
return line.trim().split(/\s+/)[0] ?? "";
|
|
2681
|
+
}
|
|
2682
|
+
function isCommandBlock(content) {
|
|
2683
|
+
return COMMAND_WORDS.includes(commandWordOf(firstNonBlankLine(content)));
|
|
2684
|
+
}
|
|
2685
|
+
function isStep0Block(content) {
|
|
2686
|
+
return firstNonBlankLine(content).startsWith("git worktree add");
|
|
2687
|
+
}
|
|
2688
|
+
function checkCommandsCarryOutput(prBody) {
|
|
2689
|
+
const errors = [];
|
|
2690
|
+
for (const [label2, num] of [
|
|
2691
|
+
["5", 5],
|
|
2692
|
+
["6", 6]
|
|
2693
|
+
]) {
|
|
2694
|
+
const section = extractNumberedSection(prBody, num);
|
|
2695
|
+
if (!section)
|
|
2696
|
+
continue;
|
|
2697
|
+
const blocks = extractFencedBlocks(section);
|
|
2698
|
+
for (let i = 0;i < blocks.length; i++) {
|
|
2699
|
+
const block = blocks[i];
|
|
2700
|
+
if (isStep0Block(block.content) || !isCommandBlock(block.content))
|
|
2701
|
+
continue;
|
|
2702
|
+
const next = blocks[i + 1];
|
|
2703
|
+
if (!next || isCommandBlock(next.content)) {
|
|
2704
|
+
errors.push(`brief-validation commands carry output: §${label2} has a command block ("${firstNonBlankLine(block.content)}") not immediately followed by a non-command output block — the very next fenced block must hold that command's actual output, not another command.`);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
return errors.length === 0 ? { status: "pass", errors: [] } : { status: "fail", errors };
|
|
2709
|
+
}
|
|
2710
|
+
var CONSUMER_TESTS_SENTINEL_RE = /consumer-tests\s*:\s*none\s*[-—–]\s*\S/i;
|
|
2711
|
+
function packagesNamedIn(text) {
|
|
2712
|
+
const re = /\bpackages\/([A-Za-z0-9_-]+)\//g;
|
|
2713
|
+
const pkgs = new Set;
|
|
2714
|
+
let m = re.exec(text);
|
|
2715
|
+
while (m !== null) {
|
|
2716
|
+
pkgs.add(m[1]);
|
|
2717
|
+
m = re.exec(text);
|
|
2718
|
+
}
|
|
2719
|
+
return [...pkgs];
|
|
2720
|
+
}
|
|
2721
|
+
function hasTestPathForConsumer(text, consumerDir) {
|
|
2722
|
+
const escaped = consumerDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2723
|
+
const re = new RegExp(`${escaped}\\/[\\w./-]*\\.test\\.[A-Za-z0-9]+`);
|
|
2724
|
+
return re.test(text);
|
|
2725
|
+
}
|
|
2726
|
+
function checkConsumerTests(prBody, consumersOf) {
|
|
2727
|
+
const section4 = extractNumberedSection(prBody, 4);
|
|
2728
|
+
if (!section4)
|
|
2729
|
+
return { status: "pass", errors: [] };
|
|
2730
|
+
if (CONSUMER_TESTS_SENTINEL_RE.test(section4))
|
|
2731
|
+
return { status: "pass", errors: [] };
|
|
2732
|
+
const errors = [];
|
|
2733
|
+
for (const pkg of packagesNamedIn(section4)) {
|
|
2734
|
+
for (const consumerDir of consumersOf(pkg)) {
|
|
2735
|
+
if (hasTestPathForConsumer(section4, consumerDir))
|
|
2736
|
+
continue;
|
|
2737
|
+
errors.push(`brief-validation consumer tests: §4 names a path under packages/${pkg}/, and ${consumerDir} depends on @attalabs/${pkg}, but no test path under ${consumerDir} is named in §4 — name one, or add \`consumer-tests: none — <reason>\`.`);
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
return errors.length === 0 ? { status: "pass", errors: [] } : { status: "fail", errors };
|
|
2741
|
+
}
|
|
2742
|
+
var CHECK_NAME_RE = /\bcheck-[a-z0-9-]+(?:\.ts)?\b/;
|
|
2743
|
+
var FORGE_WRITE_COMMAND_RE = /\bgh\s+(?:pr|issue)\s+(?:create|merge|close|comment|edit|review)\b|\bgit\s+push\b|\bvinaya\s+pr\s+(?:create|report\s+--write)\b/i;
|
|
2744
|
+
var DEFEAT_CASES_RE = /defeat cases\s*:/i;
|
|
2745
|
+
function checkDefeatCases(prBody) {
|
|
2746
|
+
const section4 = extractNumberedSection(prBody, 4);
|
|
2747
|
+
if (!section4)
|
|
2748
|
+
return { status: "pass", errors: [] };
|
|
2749
|
+
if (!CHECK_NAME_RE.test(section4) && !FORGE_WRITE_COMMAND_RE.test(section4))
|
|
2750
|
+
return { status: "pass", errors: [] };
|
|
2751
|
+
const section6 = extractNumberedSection(prBody, 6) ?? "";
|
|
2752
|
+
if (DEFEAT_CASES_RE.test(section6))
|
|
2753
|
+
return { status: "pass", errors: [] };
|
|
2754
|
+
return {
|
|
2755
|
+
status: "fail",
|
|
2756
|
+
errors: [
|
|
2757
|
+
"brief-validation defeat cases: §4 names a check or a forge-writing command, but §6 carries no `Defeat cases:` line — name the inputs that would defeat this check, or that this command must not accidentally trigger on."
|
|
2758
|
+
]
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2501
2761
|
function checkBriefSections(prBody, readTier, options = {}) {
|
|
2502
|
-
const { requireClosesN = true } = options;
|
|
2762
|
+
const { requireClosesN = true, consumersOf = () => [] } = options;
|
|
2503
2763
|
const results = [
|
|
2504
2764
|
checkTierField(prBody, readTier),
|
|
2505
2765
|
checkTestPlan(prBody),
|
|
@@ -2512,10 +2772,219 @@ function checkBriefSections(prBody, readTier, options = {}) {
|
|
|
2512
2772
|
checkAutonomyClause(prBody),
|
|
2513
2773
|
checkProjectField(prBody),
|
|
2514
2774
|
checkForField(prBody),
|
|
2775
|
+
checkNoUnpinnedCodeClaims(prBody),
|
|
2776
|
+
checkNoAgentBoxes(prBody),
|
|
2777
|
+
checkCommandsCarryOutput(prBody),
|
|
2778
|
+
checkConsumerTests(prBody, consumersOf),
|
|
2779
|
+
checkDefeatCases(prBody),
|
|
2515
2780
|
...requireClosesN ? [checkClosesN(prBody)] : []
|
|
2516
2781
|
];
|
|
2517
2782
|
return { errors: results.flatMap((r) => r.errors) };
|
|
2518
2783
|
}
|
|
2784
|
+
// ../../packages/aeg-core/src/doctrine-portability.ts
|
|
2785
|
+
var DEFAULT_SHIPS_PREFIX = "aeg-root/";
|
|
2786
|
+
var STATIC_PORTABLE_PREFIXES = [
|
|
2787
|
+
"roles/",
|
|
2788
|
+
"contracts/",
|
|
2789
|
+
"skills/",
|
|
2790
|
+
".github/",
|
|
2791
|
+
".vinaya/",
|
|
2792
|
+
".claude/",
|
|
2793
|
+
".git/",
|
|
2794
|
+
".husky/"
|
|
2795
|
+
];
|
|
2796
|
+
var EXEMPT_LITERALS = new Set([
|
|
2797
|
+
"path/inside/the/shipped/diff.ts",
|
|
2798
|
+
"path/inside/the/surface.ts",
|
|
2799
|
+
"apps/x/specs/..."
|
|
2800
|
+
]);
|
|
2801
|
+
var CITED_PATH_PATTERN = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]*)+$/;
|
|
2802
|
+
var NON_PATH_TOP_SEGMENTS = new Set(["origin", "refs", "HEAD", "vinaya", "fix"]);
|
|
2803
|
+
var VENDOR_NAME_SOURCE = "\\bclaude code\\b|\\bclaude\\b|\\banthropic\\b|\\bchatgpt\\b|\\bopenai\\b|\\bgpt\\b|\\bgemini\\b|\\bcodex\\b|\\bgrok\\b|\\bdeepseek\\b|\\bopus\\b|\\bsonnet\\b|\\bhaiku\\b";
|
|
2804
|
+
var VENDOR_EXAMPLE_START = /<!--\s*AEG:VENDOR-EXAMPLE:START\s*-->/;
|
|
2805
|
+
var VENDOR_EXAMPLE_END = /<!--\s*AEG:VENDOR-EXAMPLE:END\s*-->/;
|
|
2806
|
+
function maskVendorExampleRegion(masked) {
|
|
2807
|
+
const start = VENDOR_EXAMPLE_START.exec(masked);
|
|
2808
|
+
if (!start)
|
|
2809
|
+
return masked;
|
|
2810
|
+
const afterStart = start.index + start[0].length;
|
|
2811
|
+
const end = VENDOR_EXAMPLE_END.exec(masked.slice(afterStart));
|
|
2812
|
+
if (!end)
|
|
2813
|
+
return masked;
|
|
2814
|
+
const regionEnd = afterStart + end.index + end[0].length;
|
|
2815
|
+
const region = masked.slice(start.index, regionEnd);
|
|
2816
|
+
const blanked = region.replace(/[^\n]/g, " ");
|
|
2817
|
+
return masked.slice(0, start.index) + blanked + masked.slice(regionEnd);
|
|
2818
|
+
}
|
|
2819
|
+
function extractVendorMentions(content) {
|
|
2820
|
+
const scoped = maskVendorExampleRegion(maskCode(content));
|
|
2821
|
+
const found = [];
|
|
2822
|
+
const pattern = new RegExp(VENDOR_NAME_SOURCE, "gi");
|
|
2823
|
+
let match = pattern.exec(scoped);
|
|
2824
|
+
while (match !== null) {
|
|
2825
|
+
found.push({ name: match[0], index: match.index });
|
|
2826
|
+
match = pattern.exec(scoped);
|
|
2827
|
+
}
|
|
2828
|
+
return found;
|
|
2829
|
+
}
|
|
2830
|
+
function extractCitedPaths(content) {
|
|
2831
|
+
const found = [];
|
|
2832
|
+
const spanPattern = /`([^`\n]+)`/g;
|
|
2833
|
+
let match = spanPattern.exec(content);
|
|
2834
|
+
while (match !== null) {
|
|
2835
|
+
const cited = match[1] ?? "";
|
|
2836
|
+
const topSegment = cited.slice(0, cited.indexOf("/"));
|
|
2837
|
+
if (CITED_PATH_PATTERN.test(cited) && !NON_PATH_TOP_SEGMENTS.has(topSegment)) {
|
|
2838
|
+
found.push({ cited, index: match.index });
|
|
2839
|
+
}
|
|
2840
|
+
match = spanPattern.exec(content);
|
|
2841
|
+
}
|
|
2842
|
+
return found;
|
|
2843
|
+
}
|
|
2844
|
+
function lineAtIndex(content, index) {
|
|
2845
|
+
let line = 1;
|
|
2846
|
+
for (let i = 0;i < index; i++) {
|
|
2847
|
+
if (content.charCodeAt(i) === 10)
|
|
2848
|
+
line++;
|
|
2849
|
+
}
|
|
2850
|
+
return line;
|
|
2851
|
+
}
|
|
2852
|
+
function isPortable(cited, shipsPrefix) {
|
|
2853
|
+
if (cited.startsWith(shipsPrefix))
|
|
2854
|
+
return true;
|
|
2855
|
+
return STATIC_PORTABLE_PREFIXES.some((prefix) => cited.startsWith(prefix));
|
|
2856
|
+
}
|
|
2857
|
+
function checkDoctrinePortability(files, shipsPrefix = DEFAULT_SHIPS_PREFIX) {
|
|
2858
|
+
const findings = [];
|
|
2859
|
+
for (const file of files) {
|
|
2860
|
+
if (!file.path.startsWith(shipsPrefix))
|
|
2861
|
+
continue;
|
|
2862
|
+
for (const { cited, index } of extractCitedPaths(file.content)) {
|
|
2863
|
+
if (EXEMPT_LITERALS.has(cited))
|
|
2864
|
+
continue;
|
|
2865
|
+
if (isPortable(cited, shipsPrefix))
|
|
2866
|
+
continue;
|
|
2867
|
+
findings.push({
|
|
2868
|
+
file: file.path,
|
|
2869
|
+
line: lineAtIndex(file.content, index),
|
|
2870
|
+
cited,
|
|
2871
|
+
message: `cites "${cited}", a path that only exists in the authoring repository — not portable doctrine`,
|
|
2872
|
+
kind: "path"
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
for (const { name, index } of extractVendorMentions(file.content)) {
|
|
2876
|
+
findings.push({
|
|
2877
|
+
file: file.path,
|
|
2878
|
+
line: lineAtIndex(file.content, index),
|
|
2879
|
+
cited: name,
|
|
2880
|
+
message: `names "${name}" outside the one fenced vendor-example home (an <!-- AEG:VENDOR-EXAMPLE:START --> … <!-- AEG:VENDOR-EXAMPLE:END --> pair) — portable doctrine refers to a host generically everywhere else`,
|
|
2881
|
+
kind: "vendor-name"
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
return findings;
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// ../../packages/aeg-core/src/doctrine-no-procedures.ts
|
|
2889
|
+
function vendorExampleSpan(content) {
|
|
2890
|
+
const start = VENDOR_EXAMPLE_START.exec(content);
|
|
2891
|
+
if (!start)
|
|
2892
|
+
return null;
|
|
2893
|
+
const afterStart = start.index + start[0].length;
|
|
2894
|
+
const end = VENDOR_EXAMPLE_END.exec(content.slice(afterStart));
|
|
2895
|
+
if (!end)
|
|
2896
|
+
return null;
|
|
2897
|
+
return { start: start.index, end: afterStart + end.index + end[0].length };
|
|
2898
|
+
}
|
|
2899
|
+
function isCommandLine(line) {
|
|
2900
|
+
const word = line.trim().split(/\s+/)[0] ?? "";
|
|
2901
|
+
return COMMAND_WORDS.includes(word);
|
|
2902
|
+
}
|
|
2903
|
+
var SHELL_LANGS = new Set(["", "sh", "bash", "shell", "console"]);
|
|
2904
|
+
function procedureLineNumberAt(content, index) {
|
|
2905
|
+
return content.slice(0, index).split(`
|
|
2906
|
+
`).length;
|
|
2907
|
+
}
|
|
2908
|
+
function isTemplatePath(path) {
|
|
2909
|
+
return /(^|\/)templates\//.test(path);
|
|
2910
|
+
}
|
|
2911
|
+
function checkDoctrineNoProcedures(files) {
|
|
2912
|
+
const findings = [];
|
|
2913
|
+
for (const file of files) {
|
|
2914
|
+
if (isTemplatePath(file.path))
|
|
2915
|
+
continue;
|
|
2916
|
+
const span = vendorExampleSpan(file.content);
|
|
2917
|
+
for (const block of extractFencedBlocks(file.content)) {
|
|
2918
|
+
if (span && block.start >= span.start && block.end <= span.end)
|
|
2919
|
+
continue;
|
|
2920
|
+
if (!SHELL_LANGS.has(block.lang))
|
|
2921
|
+
continue;
|
|
2922
|
+
const commandLineCount = block.content.split(`
|
|
2923
|
+
`).filter(isCommandLine).length;
|
|
2924
|
+
if (commandLineCount >= 2) {
|
|
2925
|
+
findings.push({
|
|
2926
|
+
file: file.path,
|
|
2927
|
+
line: procedureLineNumberAt(file.content, block.start),
|
|
2928
|
+
message: `this fenced block has ${commandLineCount} shell-command lines — this sequence is a \`vinaya\` command, name it (or move it inside the \`AEG:VENDOR-EXAMPLE\` anchor / a \`templates/\` file).`
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
return findings;
|
|
2934
|
+
}
|
|
2935
|
+
// ../../packages/aeg-core/src/pr-report-density.ts
|
|
2936
|
+
function headingSectionBody(prBody, heading) {
|
|
2937
|
+
const headingRe = new RegExp(`^##\\s+${heading}\\s*$`, "im");
|
|
2938
|
+
const m = headingRe.exec(prBody);
|
|
2939
|
+
if (!m)
|
|
2940
|
+
return null;
|
|
2941
|
+
const start = m.index + m[0].length;
|
|
2942
|
+
const rest = prBody.slice(start);
|
|
2943
|
+
const nextHeading = /^#{1,2}\s/m.exec(rest);
|
|
2944
|
+
return nextHeading ? rest.slice(0, nextHeading.index) : rest;
|
|
2945
|
+
}
|
|
2946
|
+
function stripAnchorBlocks(sectionText) {
|
|
2947
|
+
let result = sectionText;
|
|
2948
|
+
for (const field of ["CLOSES", "PROJECT", "TIER", "PREMISE", "TEST-PLAN", "EVIDENCE"]) {
|
|
2949
|
+
for (;; ) {
|
|
2950
|
+
const bounds = anchoredRegionBounds(result, field);
|
|
2951
|
+
if (!bounds)
|
|
2952
|
+
break;
|
|
2953
|
+
result = result.slice(0, bounds.outerStart) + result.slice(bounds.outerEnd);
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
return result;
|
|
2957
|
+
}
|
|
2958
|
+
function paragraphCount(sectionText) {
|
|
2959
|
+
const stripped = stripCode(stripAnchorBlocks(sectionText)).trim();
|
|
2960
|
+
if (stripped.length === 0)
|
|
2961
|
+
return 0;
|
|
2962
|
+
return stripped.split(/\n[ \t]*\n/).map((block) => block.trim()).filter((block) => block.length > 0).length;
|
|
2963
|
+
}
|
|
2964
|
+
function checkSectionDensity(prBody, heading) {
|
|
2965
|
+
const body = headingSectionBody(prBody, heading);
|
|
2966
|
+
if (body === null)
|
|
2967
|
+
return { status: "pass", errors: [] };
|
|
2968
|
+
const count = paragraphCount(body);
|
|
2969
|
+
if (count <= 1)
|
|
2970
|
+
return { status: "pass", errors: [] };
|
|
2971
|
+
return {
|
|
2972
|
+
status: "fail",
|
|
2973
|
+
errors: [
|
|
2974
|
+
`pr-report-density ${heading}: "## ${heading}" holds ${count} paragraphs — the canonical PR-report form (aeg-root/roles/developer.md § PR body) requires exactly one. Collapse it to one paragraph; move the rest into a section of your own below the canonical four ("Add anything you want beneath the four sections", developer.md), a commit message, or the changeset — never into the AEG:EVIDENCE block, which is emitted only, never hand-typed.`
|
|
2975
|
+
]
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
function checkSummaryDensity(prBody) {
|
|
2979
|
+
return checkSectionDensity(prBody, "Summary");
|
|
2980
|
+
}
|
|
2981
|
+
function checkScopeDensity(prBody) {
|
|
2982
|
+
return checkSectionDensity(prBody, "Scope");
|
|
2983
|
+
}
|
|
2984
|
+
function checkPrReportDensity(prBody) {
|
|
2985
|
+
const results = [checkSummaryDensity(prBody), checkScopeDensity(prBody)];
|
|
2986
|
+
return { errors: results.flatMap((r) => r.errors) };
|
|
2987
|
+
}
|
|
2519
2988
|
// ../../packages/aeg-core/src/issue-validation.ts
|
|
2520
2989
|
function hasRationaleField(body, labelPattern) {
|
|
2521
2990
|
const re = new RegExp(`(?:\\*\\*|^#{1,4}\\s+)\\s*(?:${labelPattern})`, "im");
|
|
@@ -2767,6 +3236,17 @@ function checkD1(entries, issueToEntry, taskToEntry) {
|
|
|
2767
3236
|
const depEntry = resolveDepEntry(dep, e.trancheSlug, issueToEntry, taskToEntry);
|
|
2768
3237
|
if (!depEntry)
|
|
2769
3238
|
continue;
|
|
3239
|
+
const sameTask = depEntry.trancheSlug === e.trancheSlug && depEntry.task.id === e.task.id;
|
|
3240
|
+
const sameIssue = depEntry.task.issue !== null && e.task.issue !== null && depEntry.task.issue === e.task.issue;
|
|
3241
|
+
if (sameTask || sameIssue) {
|
|
3242
|
+
failures.push({
|
|
3243
|
+
issue: e.task.issue,
|
|
3244
|
+
tranche: e.trancheSlug,
|
|
3245
|
+
task: e.task.id,
|
|
3246
|
+
reason: `INTERNAL: parsed a self-dependency (depends-on ${dep}) — this is a parser bug in parseRationaleDeps, not a real dependency. Please report it upstream.`
|
|
3247
|
+
});
|
|
3248
|
+
continue;
|
|
3249
|
+
}
|
|
2770
3250
|
const depFacts = depEntry.facts;
|
|
2771
3251
|
const depClosed = depFacts?.issueState === "closed";
|
|
2772
3252
|
if (!depClosed) {
|
|
@@ -2910,41 +3390,24 @@ function checkClosesN2(branch, prBody, trancheFiles, taskIssueRefs) {
|
|
|
2910
3390
|
}
|
|
2911
3391
|
return { ok: true, expectedIssue };
|
|
2912
3392
|
}
|
|
2913
|
-
// ../../packages/aeg-core/src/verdict-extraction.ts
|
|
2914
|
-
var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
|
|
2915
|
-
function extractHeadSha(comment) {
|
|
2916
|
-
const m = comment.match(HEAD_SHA_PATTERN);
|
|
2917
|
-
return m ? m[1].toLowerCase() : null;
|
|
2918
|
-
}
|
|
2919
|
-
function extractVerdict(comments, valuePattern, missingLabel) {
|
|
2920
|
-
const clearHits = comments.filter((c) => valuePattern.test(c));
|
|
2921
|
-
if (clearHits.length > 0) {
|
|
2922
|
-
const latest = clearHits[clearHits.length - 1];
|
|
2923
|
-
const m = latest.match(valuePattern);
|
|
2924
|
-
return {
|
|
2925
|
-
value: m[1].toUpperCase().replace(/[_-]/g, " "),
|
|
2926
|
-
headSha: extractHeadSha(latest),
|
|
2927
|
-
danglingNote: null
|
|
2928
|
-
};
|
|
2929
|
-
}
|
|
2930
|
-
return {
|
|
2931
|
-
value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
|
|
2932
|
-
headSha: null,
|
|
2933
|
-
danglingNote: `no ${missingLabel} verdict comment found on this PR`
|
|
2934
|
-
};
|
|
2935
|
-
}
|
|
2936
|
-
function extractCodeReviewVerdict(comments) {
|
|
2937
|
-
return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(APPROVE|REQUEST[ _-]?CHANGES|LGTM)(?![A-Za-z0-9])/im, "code-reviewer");
|
|
2938
|
-
}
|
|
2939
|
-
function extractSecurityReviewVerdict(comments) {
|
|
2940
|
-
return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(PASS|FAIL)(?![A-Za-z0-9])/im, "security-review");
|
|
2941
|
-
}
|
|
2942
3393
|
// ../../packages/aeg-core/src/review-gate.ts
|
|
2943
3394
|
function isBoundToHead(extraction, headSha) {
|
|
2944
3395
|
if (!extraction.headSha)
|
|
2945
3396
|
return false;
|
|
2946
3397
|
return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
|
|
2947
3398
|
}
|
|
3399
|
+
function isBoundByPatchIdentity(extraction, headSha, patchIdOf) {
|
|
3400
|
+
if (patchIdOf === undefined || !extraction.headSha)
|
|
3401
|
+
return false;
|
|
3402
|
+
const judged = patchIdOf(extraction.headSha);
|
|
3403
|
+
const current = patchIdOf(headSha);
|
|
3404
|
+
if (judged === null || current === null)
|
|
3405
|
+
return false;
|
|
3406
|
+
return judged === current;
|
|
3407
|
+
}
|
|
3408
|
+
function isBoundToPatch(extraction, headSha, patchIdOf) {
|
|
3409
|
+
return isBoundToHead(extraction, headSha) || isBoundByPatchIdentity(extraction, headSha, patchIdOf);
|
|
3410
|
+
}
|
|
2948
3411
|
function checkReviewGate(input) {
|
|
2949
3412
|
const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
|
|
2950
3413
|
const waived = isWaiverLabelActorVerified({
|
|
@@ -2960,6 +3423,8 @@ function checkReviewGate(input) {
|
|
|
2960
3423
|
waived: true
|
|
2961
3424
|
};
|
|
2962
3425
|
}
|
|
3426
|
+
const reportedMechanicalChecks = input.mechanicalChecks.filter((c) => c.bucket !== "skipping" && c.bucket !== "neutral");
|
|
3427
|
+
const mechanicalChecksClean = reportedMechanicalChecks.length > 0 && reportedMechanicalChecks.every((c) => c.bucket === "pass");
|
|
2963
3428
|
const verified = input.comments.filter((c) => isPrincipal(c.author, principalAllowlist));
|
|
2964
3429
|
const ignoredCount = input.comments.filter((c) => !isPrincipal(c.author, principalAllowlist) && c.body.includes("VERDICT")).length;
|
|
2965
3430
|
const verifiedBodies = verified.map((c) => c.body);
|
|
@@ -2967,12 +3432,12 @@ function checkReviewGate(input) {
|
|
|
2967
3432
|
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2968
3433
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2969
3434
|
const securityClean = security.value === "PASS";
|
|
2970
|
-
const codeReviewBound =
|
|
2971
|
-
const securityBound =
|
|
2972
|
-
if (codeReviewClean && codeReviewBound && securityClean && securityBound) {
|
|
3435
|
+
const codeReviewBound = isBoundToPatch(codeReview, input.headSha, input.patchIdOf);
|
|
3436
|
+
const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
|
|
3437
|
+
if (codeReviewClean && codeReviewBound && securityClean && securityBound && mechanicalChecksClean) {
|
|
2973
3438
|
return {
|
|
2974
3439
|
verdict: "pass",
|
|
2975
|
-
reason: `code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS, both covering head ${input.headSha}.`,
|
|
3440
|
+
reason: `code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS, both covering head ${input.headSha}, and every reported mechanical check is green.`,
|
|
2976
3441
|
waived: false
|
|
2977
3442
|
};
|
|
2978
3443
|
}
|
|
@@ -2987,6 +3452,9 @@ function checkReviewGate(input) {
|
|
|
2987
3452
|
} else if (!securityBound) {
|
|
2988
3453
|
problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
|
|
2989
3454
|
}
|
|
3455
|
+
if (!mechanicalChecksClean) {
|
|
3456
|
+
problems.push(reportedMechanicalChecks.length === 0 ? "no mechanical checks have reported for this head yet" : `mechanical check(s) not green: ${reportedMechanicalChecks.filter((c) => c.bucket !== "pass").map((c) => `${c.name} (${c.bucket})`).join(", ")}`);
|
|
3457
|
+
}
|
|
2990
3458
|
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2991
3459
|
return {
|
|
2992
3460
|
verdict: "fail",
|
|
@@ -3266,72 +3734,6 @@ function compareToBaseline(current, baseline) {
|
|
|
3266
3734
|
perTool
|
|
3267
3735
|
};
|
|
3268
3736
|
}
|
|
3269
|
-
// ../../packages/aeg-core/src/doctrine-portability.ts
|
|
3270
|
-
var DEFAULT_SHIPS_PREFIX = "aeg-root/";
|
|
3271
|
-
var STATIC_PORTABLE_PREFIXES = [
|
|
3272
|
-
"roles/",
|
|
3273
|
-
"contracts/",
|
|
3274
|
-
"skills/",
|
|
3275
|
-
".github/",
|
|
3276
|
-
".vinaya/",
|
|
3277
|
-
".claude/",
|
|
3278
|
-
".git/",
|
|
3279
|
-
".husky/"
|
|
3280
|
-
];
|
|
3281
|
-
var EXEMPT_LITERALS = new Set([
|
|
3282
|
-
"path/inside/the/shipped/diff.ts",
|
|
3283
|
-
"path/inside/the/surface.ts",
|
|
3284
|
-
"apps/x/specs/..."
|
|
3285
|
-
]);
|
|
3286
|
-
var CITED_PATH_PATTERN = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]*)+$/;
|
|
3287
|
-
var NON_PATH_TOP_SEGMENTS = new Set(["origin", "refs", "HEAD", "vinaya", "fix"]);
|
|
3288
|
-
function extractCitedPaths(content) {
|
|
3289
|
-
const found = [];
|
|
3290
|
-
const spanPattern = /`([^`\n]+)`/g;
|
|
3291
|
-
let match = spanPattern.exec(content);
|
|
3292
|
-
while (match !== null) {
|
|
3293
|
-
const cited = match[1] ?? "";
|
|
3294
|
-
const topSegment = cited.slice(0, cited.indexOf("/"));
|
|
3295
|
-
if (CITED_PATH_PATTERN.test(cited) && !NON_PATH_TOP_SEGMENTS.has(topSegment)) {
|
|
3296
|
-
found.push({ cited, index: match.index });
|
|
3297
|
-
}
|
|
3298
|
-
match = spanPattern.exec(content);
|
|
3299
|
-
}
|
|
3300
|
-
return found;
|
|
3301
|
-
}
|
|
3302
|
-
function lineAtIndex(content, index) {
|
|
3303
|
-
let line = 1;
|
|
3304
|
-
for (let i = 0;i < index; i++) {
|
|
3305
|
-
if (content.charCodeAt(i) === 10)
|
|
3306
|
-
line++;
|
|
3307
|
-
}
|
|
3308
|
-
return line;
|
|
3309
|
-
}
|
|
3310
|
-
function isPortable(cited, shipsPrefix) {
|
|
3311
|
-
if (cited.startsWith(shipsPrefix))
|
|
3312
|
-
return true;
|
|
3313
|
-
return STATIC_PORTABLE_PREFIXES.some((prefix) => cited.startsWith(prefix));
|
|
3314
|
-
}
|
|
3315
|
-
function checkDoctrinePortability(files, shipsPrefix = DEFAULT_SHIPS_PREFIX) {
|
|
3316
|
-
const findings = [];
|
|
3317
|
-
for (const file of files) {
|
|
3318
|
-
if (!file.path.startsWith(shipsPrefix))
|
|
3319
|
-
continue;
|
|
3320
|
-
for (const { cited, index } of extractCitedPaths(file.content)) {
|
|
3321
|
-
if (EXEMPT_LITERALS.has(cited))
|
|
3322
|
-
continue;
|
|
3323
|
-
if (isPortable(cited, shipsPrefix))
|
|
3324
|
-
continue;
|
|
3325
|
-
findings.push({
|
|
3326
|
-
file: file.path,
|
|
3327
|
-
line: lineAtIndex(file.content, index),
|
|
3328
|
-
cited,
|
|
3329
|
-
message: `cites "${cited}", a path that only exists in the authoring repository — not portable doctrine`
|
|
3330
|
-
});
|
|
3331
|
-
}
|
|
3332
|
-
}
|
|
3333
|
-
return findings;
|
|
3334
|
-
}
|
|
3335
3737
|
// ../../packages/aeg-core/src/reader-resolvable-prose.ts
|
|
3336
3738
|
var SHIPS_PREFIX = "aeg-root/";
|
|
3337
3739
|
function shipsArchivePrefix(shipsPrefix) {
|
|
@@ -3667,6 +4069,12 @@ function scanRetiredVocabulary(files) {
|
|
|
3667
4069
|
function isHandClosedByRecognizedPrincipal(dep, principalAllowlist) {
|
|
3668
4070
|
return dep.issueState === "closed" && dep.stateReason === "completed" && isPrincipal(dep.closedByActor ?? null, principalAllowlist);
|
|
3669
4071
|
}
|
|
4072
|
+
function isSelfDependency(dep, task, issue) {
|
|
4073
|
+
if (dep.issue !== null && issue !== null && dep.issue === issue.number)
|
|
4074
|
+
return true;
|
|
4075
|
+
const bare = dep.id.trim().replace(/^#/, "");
|
|
4076
|
+
return bare.toLowerCase() === task.id.trim().toLowerCase();
|
|
4077
|
+
}
|
|
3670
4078
|
function checkDispatchReadiness(input) {
|
|
3671
4079
|
const { trancheSlug, task } = input;
|
|
3672
4080
|
const taskLabel = `task ${task.id} (tranche ${trancheSlug})`;
|
|
@@ -3681,6 +4089,11 @@ function checkDispatchReadiness(input) {
|
|
|
3681
4089
|
blockers.push(`dispatch-gate rationale: Issue #${input.issue.number} for ${taskLabel} fails the rationale gate (checkIssueRationale) — the Planner must complete the eight-field rationale before this task is dispatchable.`);
|
|
3682
4090
|
}
|
|
3683
4091
|
for (const dep of input.dependsOn) {
|
|
4092
|
+
if (isSelfDependency(dep, task, input.issue)) {
|
|
4093
|
+
const issueStr = input.issue !== null ? `#${input.issue.number}` : "?";
|
|
4094
|
+
blockers.push(`dispatch-gate INTERNAL: parsed a self-dependency for task ${task.id} (${issueStr}) — this is a parser bug in parseRationaleDeps, not a real dependency. Please report it upstream. Re-run once the rationale is corrected or the fix ships.`);
|
|
4095
|
+
continue;
|
|
4096
|
+
}
|
|
3684
4097
|
if (dep.resolved === false) {
|
|
3685
4098
|
blockers.push(`dispatch-gate depends-on: ${taskLabel} depends on "${dep.id}", which is UNRESOLVABLE — the resolver could not find a matching tranche/task/Issue for this edge (not a claim about merge status). Not dispatchable until the edge is corrected.`);
|
|
3686
4099
|
continue;
|
|
@@ -3920,27 +4333,27 @@ function evaluateTestPlanGate(body, branch) {
|
|
|
3920
4333
|
};
|
|
3921
4334
|
}
|
|
3922
4335
|
const checkboxLines = section.split(`
|
|
3923
|
-
`).map((line) => line.trimStart()).filter((line) => /^[-*]\s+\[[ xX]\]
|
|
4336
|
+
`).map((line) => line.trimStart()).filter((line) => /^[-*]\s+\[[ xX]\]\s*\*{0,2}\[principal\]\*{0,2}/i.test(line));
|
|
3924
4337
|
if (checkboxLines.length === 0) {
|
|
3925
4338
|
return {
|
|
3926
4339
|
verdict: "pass",
|
|
3927
4340
|
messages: [
|
|
3928
|
-
"Test Plan section found but has no checkbox items.",
|
|
4341
|
+
"Test Plan section found but has no `[principal]` checkbox items.",
|
|
3929
4342
|
"PASS (advisory) — the section may be empty or expressed in prose; verification is the Principal's call."
|
|
3930
4343
|
]
|
|
3931
4344
|
};
|
|
3932
4345
|
}
|
|
3933
4346
|
const unchecked = checkboxLines.filter((line) => /^[-*]\s+\[\s\]/.test(line));
|
|
3934
4347
|
const checked = checkboxLines.filter((line) => /^[-*]\s+\[[xX]\]/.test(line));
|
|
3935
|
-
const messages = [`Test Plan items: ${checked.length} ticked, ${unchecked.length} unticked.`];
|
|
4348
|
+
const messages = [`Test Plan [principal] items: ${checked.length} ticked, ${unchecked.length} unticked.`];
|
|
3936
4349
|
if (unchecked.length > 0) {
|
|
3937
|
-
messages.push("", "FAIL — the following Test Plan items are unticked:");
|
|
4350
|
+
messages.push("", "FAIL — the following [principal] Test Plan items are unticked:");
|
|
3938
4351
|
for (const line of unchecked)
|
|
3939
4352
|
messages.push(` ${line}`);
|
|
3940
|
-
messages.push("", "Per aeg-root/roles/developer.md (Verification), a PR is not mergeable while any Test Plan box is unticked.", "
|
|
4353
|
+
messages.push("", "Per aeg-root/roles/developer.md (Verification), a PR is not mergeable while any [principal] Test Plan box is unticked.", "The Principal runs the item in a real signed-in browser and ticks the box.", "Note: editing the PR body does not retrigger most workflows. If your PR body changes do not surface here, push an empty commit to re-run.");
|
|
3941
4354
|
return { verdict: "fail", messages };
|
|
3942
4355
|
}
|
|
3943
|
-
messages.push("All Test Plan items ticked — runtime verification complete.", "PASS.");
|
|
4356
|
+
messages.push("All [principal] Test Plan items ticked — runtime verification complete.", "PASS.");
|
|
3944
4357
|
return { verdict: "pass", messages };
|
|
3945
4358
|
}
|
|
3946
4359
|
// ../../packages/aeg-core/src/workspace-escape.ts
|
|
@@ -4020,6 +4433,313 @@ function findWorkspaceEscapes(files, knownPaths, workspaceDirs = DEFAULT_WORKSPA
|
|
|
4020
4433
|
}
|
|
4021
4434
|
return findings;
|
|
4022
4435
|
}
|
|
4436
|
+
// src/lib/diff-evidence.ts
|
|
4437
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
4438
|
+
import { join as join2, relative, resolve as resolve2 } from "node:path";
|
|
4439
|
+
|
|
4440
|
+
// src/lib/config.ts
|
|
4441
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
4442
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, realpathSync, writeFileSync } from "node:fs";
|
|
4443
|
+
import { homedir } from "node:os";
|
|
4444
|
+
import { dirname, join, resolve } from "node:path";
|
|
4445
|
+
import { z } from "zod";
|
|
4446
|
+
|
|
4447
|
+
// src/lib/agent-vendors.ts
|
|
4448
|
+
var AGENT_VENDORS = ["skills", "claude", "gemini"];
|
|
4449
|
+
|
|
4450
|
+
// src/lib/config.ts
|
|
4451
|
+
var EnvEntrySchema = z.union([
|
|
4452
|
+
z.literal(true),
|
|
4453
|
+
z.object({ optional: z.literal(true) }),
|
|
4454
|
+
z.object({ anyOf: z.array(z.string()).min(2) }),
|
|
4455
|
+
z.string()
|
|
4456
|
+
]);
|
|
4457
|
+
var CheckEntrySchema = z.object({
|
|
4458
|
+
run: z.string(),
|
|
4459
|
+
scope: z.enum(["diff", "full"]),
|
|
4460
|
+
include: z.array(z.string()).optional(),
|
|
4461
|
+
args: z.array(z.string()).optional(),
|
|
4462
|
+
timeoutMs: z.number().optional(),
|
|
4463
|
+
env: z.record(z.string(), EnvEntrySchema).optional(),
|
|
4464
|
+
requiresOpenPr: z.boolean().optional(),
|
|
4465
|
+
ownWorkflow: z.boolean().optional()
|
|
4466
|
+
}).superRefine((entry2, ctx) => {
|
|
4467
|
+
if (!entry2.env)
|
|
4468
|
+
return;
|
|
4469
|
+
for (const [key, value] of Object.entries(entry2.env)) {
|
|
4470
|
+
if (typeof value !== "object" || value === null || !("anyOf" in value))
|
|
4471
|
+
continue;
|
|
4472
|
+
const members = value.anyOf;
|
|
4473
|
+
if (new Set(members).size !== members.length) {
|
|
4474
|
+
ctx.addIssue({
|
|
4475
|
+
code: z.ZodIssueCode.custom,
|
|
4476
|
+
message: `env.${key}.anyOf has duplicate members`,
|
|
4477
|
+
path: ["env", key, "anyOf"]
|
|
4478
|
+
});
|
|
4479
|
+
}
|
|
4480
|
+
if (!members.includes(key)) {
|
|
4481
|
+
ctx.addIssue({
|
|
4482
|
+
code: z.ZodIssueCode.custom,
|
|
4483
|
+
message: `env.${key}.anyOf must include "${key}" itself as a member`,
|
|
4484
|
+
path: ["env", key, "anyOf"]
|
|
4485
|
+
});
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
});
|
|
4489
|
+
var RoleEntrySchema = z.object({
|
|
4490
|
+
contract: z.string().refine((p) => p.includes("/"), {
|
|
4491
|
+
message: 'must be a path (contain at least one "/"), not a bare filename'
|
|
4492
|
+
})
|
|
4493
|
+
});
|
|
4494
|
+
var BRIEF_BUILTINS = [
|
|
4495
|
+
"tier",
|
|
4496
|
+
"testPlan",
|
|
4497
|
+
"testPlanExclusivity",
|
|
4498
|
+
"principalPlaceholder",
|
|
4499
|
+
"surfaceMap",
|
|
4500
|
+
"docUpdateList",
|
|
4501
|
+
"worktreeStep0",
|
|
4502
|
+
"stopConditions",
|
|
4503
|
+
"autonomyClause",
|
|
4504
|
+
"project",
|
|
4505
|
+
"for",
|
|
4506
|
+
"closesN",
|
|
4507
|
+
"premiseCoverage",
|
|
4508
|
+
"issueRationale",
|
|
4509
|
+
"milestoneShape"
|
|
4510
|
+
];
|
|
4511
|
+
var BriefSectionSchema = z.union([
|
|
4512
|
+
z.object({ builtin: z.enum(BRIEF_BUILTINS) }),
|
|
4513
|
+
z.object({ heading: z.string().min(1), name: z.string().optional() }),
|
|
4514
|
+
z.object({ field: z.string().min(1), name: z.string().optional() }),
|
|
4515
|
+
z.object({ phrase: z.string().min(1), name: z.string().optional() })
|
|
4516
|
+
]);
|
|
4517
|
+
var BriefSchemaSchema = z.object({
|
|
4518
|
+
pr: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
|
|
4519
|
+
issue: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
|
|
4520
|
+
milestone: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
|
|
4521
|
+
ack: z.array(z.enum(BRIEF_BUILTINS)).optional()
|
|
4522
|
+
});
|
|
4523
|
+
function isSafeRepoRelPath(p) {
|
|
4524
|
+
if (p.length === 0)
|
|
4525
|
+
return false;
|
|
4526
|
+
if (p.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(p))
|
|
4527
|
+
return false;
|
|
4528
|
+
return p.split(/[\\/]/).every((seg) => seg !== ".." && seg !== "");
|
|
4529
|
+
}
|
|
4530
|
+
var SafeRepoRelPath = z.string().refine(isSafeRepoRelPath, {
|
|
4531
|
+
message: "must be a repo-root-relative path with no `..` segment or absolute root"
|
|
4532
|
+
});
|
|
4533
|
+
var CANONICAL_HOOK_BLOCK_PREFIXES = [".git/", ".husky/", ".vinaya/hooks/", ".claude/hooks/"];
|
|
4534
|
+
function isCanonicalHookBlockPath(p) {
|
|
4535
|
+
return CANONICAL_HOOK_BLOCK_PREFIXES.some((prefix) => p.startsWith(prefix) && p.length > prefix.length);
|
|
4536
|
+
}
|
|
4537
|
+
var ManagedHookBlockPath = SafeRepoRelPath.refine(isCanonicalHookBlockPath, {
|
|
4538
|
+
message: `must start with one of ${CANONICAL_HOOK_BLOCK_PREFIXES.join(", ")} (byte-exact, case-sensitive)`
|
|
4539
|
+
});
|
|
4540
|
+
var ManagedBlockRecordSchema = z.object({
|
|
4541
|
+
path: ManagedHookBlockPath,
|
|
4542
|
+
marker: z.string(),
|
|
4543
|
+
comment: z.enum(["hash", "html"])
|
|
4544
|
+
});
|
|
4545
|
+
var ManagedManifestSchema = z.object({
|
|
4546
|
+
version: z.number().int().positive(),
|
|
4547
|
+
files: z.array(SafeRepoRelPath),
|
|
4548
|
+
blocks: z.array(ManagedBlockRecordSchema),
|
|
4549
|
+
labels: z.array(z.string()),
|
|
4550
|
+
agents: z.array(z.enum(AGENT_VENDORS)).optional()
|
|
4551
|
+
});
|
|
4552
|
+
var ProjectEntrySchema = z.object({
|
|
4553
|
+
name: z.string().min(1),
|
|
4554
|
+
description: z.string().min(1).optional(),
|
|
4555
|
+
path: z.string().min(1).optional()
|
|
4556
|
+
});
|
|
4557
|
+
function parseTokensCollectDeclaration(value) {
|
|
4558
|
+
const m = value.trim().match(/^(\S+)\s+(\S+)$/);
|
|
4559
|
+
if (!m)
|
|
4560
|
+
return null;
|
|
4561
|
+
const [, interpreter, script] = m;
|
|
4562
|
+
if (!isSafeRepoRelPath(script))
|
|
4563
|
+
return null;
|
|
4564
|
+
return { interpreter, script };
|
|
4565
|
+
}
|
|
4566
|
+
var VinayaConfigSchema = z.object({
|
|
4567
|
+
rings: z.object({
|
|
4568
|
+
ring1_forgeWriteInterception: z.boolean(),
|
|
4569
|
+
ring2_asyncAudits: z.boolean()
|
|
4570
|
+
}).optional(),
|
|
4571
|
+
checks: z.record(z.string(), CheckEntrySchema).optional(),
|
|
4572
|
+
roles: z.record(z.string(), RoleEntrySchema).optional(),
|
|
4573
|
+
briefSchema: BriefSchemaSchema.optional(),
|
|
4574
|
+
managed: ManagedManifestSchema.optional(),
|
|
4575
|
+
principals: z.array(z.string()).min(1).optional(),
|
|
4576
|
+
releaseActor: z.string().min(1).optional(),
|
|
4577
|
+
ci: z.object({ setup: z.string().min(1) }).optional(),
|
|
4578
|
+
tokens: z.object({
|
|
4579
|
+
collect: z.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
|
|
4580
|
+
message: 'tokens.collect must be exactly "<interpreter> <repo-relative-script-path>" — two whitespace-separated tokens, no flags, no shell syntax, no quoting'
|
|
4581
|
+
})
|
|
4582
|
+
}).optional(),
|
|
4583
|
+
blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional(),
|
|
4584
|
+
proseGates: z.object({
|
|
4585
|
+
doctrineRoot: z.string().min(1).optional(),
|
|
4586
|
+
readerFacingPrefix: z.string().min(1).optional(),
|
|
4587
|
+
readerFacingSuffix: z.string().min(1).optional(),
|
|
4588
|
+
legacySlugDir: z.string().min(1).optional()
|
|
4589
|
+
}).optional(),
|
|
4590
|
+
projects: z.array(ProjectEntrySchema).optional()
|
|
4591
|
+
});
|
|
4592
|
+
var GLOBAL_VINAYA_HOME = join(homedir(), ".vinaya");
|
|
4593
|
+
var GLOBAL_CONFIG_PATH = join(GLOBAL_VINAYA_HOME, "config.json");
|
|
4594
|
+
var LOCAL_CONFIG_FILENAME = "vinaya.config.json";
|
|
4595
|
+
function findLocalConfig() {
|
|
4596
|
+
let dir = process.cwd();
|
|
4597
|
+
while (true) {
|
|
4598
|
+
const candidate = join(dir, LOCAL_CONFIG_FILENAME);
|
|
4599
|
+
if (existsSync(candidate))
|
|
4600
|
+
return candidate;
|
|
4601
|
+
if (existsSync(join(dir, ".git")))
|
|
4602
|
+
return null;
|
|
4603
|
+
const parent = dirname(dir);
|
|
4604
|
+
if (parent === dir)
|
|
4605
|
+
return null;
|
|
4606
|
+
dir = parent;
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
function configPath() {
|
|
4610
|
+
const local = findLocalConfig();
|
|
4611
|
+
if (local)
|
|
4612
|
+
return local;
|
|
4613
|
+
if (existsSync(GLOBAL_CONFIG_PATH))
|
|
4614
|
+
return GLOBAL_CONFIG_PATH;
|
|
4615
|
+
return null;
|
|
4616
|
+
}
|
|
4617
|
+
function globalChecksIgnoredWarning(path) {
|
|
4618
|
+
return `${path}: "checks" registration in the global config is ignored — checks may only be registered from a repo-local vinaya.config.json.`;
|
|
4619
|
+
}
|
|
4620
|
+
function globalPrincipalsIgnoredWarning(path) {
|
|
4621
|
+
return `${path}: "principals" in the global config is ignored — principals may only be declared from a repo-local vinaya.config.json.`;
|
|
4622
|
+
}
|
|
4623
|
+
function globalRolesIgnoredWarning(path) {
|
|
4624
|
+
return `${path}: "roles" registration in the global config is ignored — roles may only be registered from a repo-local vinaya.config.json.`;
|
|
4625
|
+
}
|
|
4626
|
+
function globalReleaseActorIgnoredWarning(path) {
|
|
4627
|
+
return `${path}: "releaseActor" in the global config is ignored — releaseActor may only be declared from a repo-local vinaya.config.json.`;
|
|
4628
|
+
}
|
|
4629
|
+
function globalTokensCollectIgnoredWarning(path) {
|
|
4630
|
+
return `${path}: "tokens" in the global config is ignored — tokens.collect may only be declared from a repo-local vinaya.config.json.`;
|
|
4631
|
+
}
|
|
4632
|
+
function stripGlobalOnlyKeys(config, path) {
|
|
4633
|
+
if (path !== GLOBAL_CONFIG_PATH)
|
|
4634
|
+
return config;
|
|
4635
|
+
let result = config;
|
|
4636
|
+
if (result.checks && Object.keys(result.checks).length > 0) {
|
|
4637
|
+
console.error(`⚠ ${globalChecksIgnoredWarning(path)}`);
|
|
4638
|
+
result = { ...result, checks: undefined };
|
|
4639
|
+
}
|
|
4640
|
+
if (result.roles && Object.keys(result.roles).length > 0) {
|
|
4641
|
+
console.error(`⚠ ${globalRolesIgnoredWarning(path)}`);
|
|
4642
|
+
result = { ...result, roles: undefined };
|
|
4643
|
+
}
|
|
4644
|
+
if (result.principals && result.principals.length > 0) {
|
|
4645
|
+
console.error(`⚠ ${globalPrincipalsIgnoredWarning(path)}`);
|
|
4646
|
+
result = { ...result, principals: undefined };
|
|
4647
|
+
}
|
|
4648
|
+
if (result.releaseActor) {
|
|
4649
|
+
console.error(`⚠ ${globalReleaseActorIgnoredWarning(path)}`);
|
|
4650
|
+
result = { ...result, releaseActor: undefined };
|
|
4651
|
+
}
|
|
4652
|
+
if (result.tokens) {
|
|
4653
|
+
console.error(`⚠ ${globalTokensCollectIgnoredWarning(path)}`);
|
|
4654
|
+
result = { ...result, tokens: undefined };
|
|
4655
|
+
}
|
|
4656
|
+
return result;
|
|
4657
|
+
}
|
|
4658
|
+
function resolvePrincipalAllowlist(config) {
|
|
4659
|
+
return config?.principals ?? PRINCIPAL_ALLOWLIST;
|
|
4660
|
+
}
|
|
4661
|
+
function resolveReleaseActor(config) {
|
|
4662
|
+
return config?.releaseActor ?? DEFAULT_RELEASE_ACTOR;
|
|
4663
|
+
}
|
|
4664
|
+
function trustAnchorRepo() {
|
|
4665
|
+
const wellFormed = (slug) => /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
|
|
4666
|
+
const fromRunner = process.env.GITHUB_REPOSITORY?.trim();
|
|
4667
|
+
if (fromRunner) {
|
|
4668
|
+
const validated = wellFormed(fromRunner);
|
|
4669
|
+
if (validated)
|
|
4670
|
+
return validated;
|
|
4671
|
+
}
|
|
4672
|
+
try {
|
|
4673
|
+
const url = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
4674
|
+
encoding: "utf-8",
|
|
4675
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4676
|
+
}).trim();
|
|
4677
|
+
const m = url.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/);
|
|
4678
|
+
return m?.[1] && m[2] ? wellFormed(`${m[1]}/${m[2]}`) : null;
|
|
4679
|
+
} catch {
|
|
4680
|
+
return null;
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
function ghFetchTrustAnchorConfig() {
|
|
4684
|
+
const repo = trustAnchorRepo();
|
|
4685
|
+
if (!repo)
|
|
4686
|
+
throw new Error("could not resolve repo identity for the trust-anchor read");
|
|
4687
|
+
return execFileSync2("gh", ["api", `repos/${repo}/contents/${LOCAL_CONFIG_FILENAME}`, "--jq", ".content"], {
|
|
4688
|
+
encoding: "utf-8",
|
|
4689
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4690
|
+
});
|
|
4691
|
+
}
|
|
4692
|
+
function firstLine(err) {
|
|
4693
|
+
return err?.message?.split(`
|
|
4694
|
+
`)[0] ?? "unknown error";
|
|
4695
|
+
}
|
|
4696
|
+
function isMissingFileError(err) {
|
|
4697
|
+
const stderr = err?.stderr;
|
|
4698
|
+
const haystack = [
|
|
4699
|
+
err?.message ?? "",
|
|
4700
|
+
typeof stderr === "string" ? stderr : stderr?.toString() ?? ""
|
|
4701
|
+
].join(`
|
|
4702
|
+
`);
|
|
4703
|
+
return /\b404\b|not found/i.test(haystack);
|
|
4704
|
+
}
|
|
4705
|
+
function loadTrustAnchorConfig(fetcher = ghFetchTrustAnchorConfig) {
|
|
4706
|
+
const warn = (why) => process.stdout.write(`⚠ could not read the trust-anchor config (\`principals\`/\`releaseActor\`) from the default branch (${why}) — falling back to vinaya's built-in defaults.
|
|
4707
|
+
`);
|
|
4708
|
+
let base64;
|
|
4709
|
+
try {
|
|
4710
|
+
base64 = fetcher().trim();
|
|
4711
|
+
} catch (err) {
|
|
4712
|
+
if (!isMissingFileError(err))
|
|
4713
|
+
warn(firstLine(err));
|
|
4714
|
+
return null;
|
|
4715
|
+
}
|
|
4716
|
+
if (!base64)
|
|
4717
|
+
return null;
|
|
4718
|
+
try {
|
|
4719
|
+
const raw = Buffer.from(base64, "base64").toString("utf-8");
|
|
4720
|
+
const parsed = VinayaConfigSchema.safeParse(JSON.parse(raw));
|
|
4721
|
+
if (parsed.success)
|
|
4722
|
+
return parsed.data;
|
|
4723
|
+
warn("it did not satisfy the config schema");
|
|
4724
|
+
return null;
|
|
4725
|
+
} catch {
|
|
4726
|
+
warn("it was not readable as JSON");
|
|
4727
|
+
return null;
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4730
|
+
function loadConfig() {
|
|
4731
|
+
const path = configPath();
|
|
4732
|
+
if (!path)
|
|
4733
|
+
return null;
|
|
4734
|
+
try {
|
|
4735
|
+
const raw = JSON.parse(readFileSync2(path, "utf-8"));
|
|
4736
|
+
return stripGlobalOnlyKeys(VinayaConfigSchema.parse(raw), path);
|
|
4737
|
+
} catch {
|
|
4738
|
+
return null;
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4741
|
+
var TOKENS_COLLECT_TRUST_PATH = join(GLOBAL_VINAYA_HOME, "tokens-collect-trust.json");
|
|
4742
|
+
|
|
4023
4743
|
// src/checks/contract.ts
|
|
4024
4744
|
var CHECK_SCHEMA_VERSION = 1;
|
|
4025
4745
|
function emitCheckError(error) {
|
|
@@ -4027,6 +4747,125 @@ function emitCheckError(error) {
|
|
|
4027
4747
|
`);
|
|
4028
4748
|
}
|
|
4029
4749
|
|
|
4750
|
+
// src/commands/review-post.ts
|
|
4751
|
+
function parseChangedLineRanges(diffOutput) {
|
|
4752
|
+
const result = {};
|
|
4753
|
+
let currentFile = null;
|
|
4754
|
+
for (const line of diffOutput.split(`
|
|
4755
|
+
`)) {
|
|
4756
|
+
if (line.startsWith("+++ ")) {
|
|
4757
|
+
const path = line.slice(4).trim();
|
|
4758
|
+
currentFile = path === "/dev/null" ? null : path.replace(/^b\//, "");
|
|
4759
|
+
continue;
|
|
4760
|
+
}
|
|
4761
|
+
if (line.startsWith("@@") && currentFile) {
|
|
4762
|
+
const m = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
|
4763
|
+
if (m) {
|
|
4764
|
+
const start = Number(m[1]);
|
|
4765
|
+
const count = m[2] !== undefined ? Number(m[2]) : 1;
|
|
4766
|
+
if (count > 0) {
|
|
4767
|
+
if (!result[currentFile])
|
|
4768
|
+
result[currentFile] = [];
|
|
4769
|
+
result[currentFile].push([start, start + count - 1]);
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
}
|
|
4773
|
+
}
|
|
4774
|
+
return result;
|
|
4775
|
+
}
|
|
4776
|
+
|
|
4777
|
+
// src/lib/diff-evidence.ts
|
|
4778
|
+
function fileDiffAgainst(ref, path) {
|
|
4779
|
+
let out;
|
|
4780
|
+
try {
|
|
4781
|
+
out = execFileSync3("git", ["diff", `${ref}...HEAD`, "--", path], {
|
|
4782
|
+
encoding: "utf8",
|
|
4783
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4784
|
+
}).trim();
|
|
4785
|
+
} catch {
|
|
4786
|
+
return null;
|
|
4787
|
+
}
|
|
4788
|
+
return out === "" ? null : out;
|
|
4789
|
+
}
|
|
4790
|
+
function revParse(ref) {
|
|
4791
|
+
try {
|
|
4792
|
+
return execFileSync3("git", ["rev-parse", "--verify", ref], {
|
|
4793
|
+
encoding: "utf8",
|
|
4794
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4795
|
+
}).trim();
|
|
4796
|
+
} catch {
|
|
4797
|
+
return null;
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
function repoRoot() {
|
|
4801
|
+
try {
|
|
4802
|
+
return execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
4803
|
+
encoding: "utf8",
|
|
4804
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4805
|
+
}).trim();
|
|
4806
|
+
} catch {
|
|
4807
|
+
return null;
|
|
4808
|
+
}
|
|
4809
|
+
}
|
|
4810
|
+
function changedFiles(base) {
|
|
4811
|
+
let out;
|
|
4812
|
+
try {
|
|
4813
|
+
out = execFileSync3("git", ["diff", "--name-only", `${base}...HEAD`], {
|
|
4814
|
+
encoding: "utf8",
|
|
4815
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4816
|
+
}).trim();
|
|
4817
|
+
} catch {
|
|
4818
|
+
return null;
|
|
4819
|
+
}
|
|
4820
|
+
return out === "" ? [] : out.split(`
|
|
4821
|
+
`).map((s) => s.trim()).filter(Boolean);
|
|
4822
|
+
}
|
|
4823
|
+
function resolveChangedFiles(base = process.env.BASE_SHA || "origin/main") {
|
|
4824
|
+
return resolveDiff(base)?.files ?? null;
|
|
4825
|
+
}
|
|
4826
|
+
function resolveDiff(base = process.env.BASE_SHA || "origin/main") {
|
|
4827
|
+
const root = repoRoot();
|
|
4828
|
+
if (root === null)
|
|
4829
|
+
return null;
|
|
4830
|
+
const head = revParse("HEAD");
|
|
4831
|
+
for (const ref of [base, "main"]) {
|
|
4832
|
+
const resolved = revParse(ref);
|
|
4833
|
+
if (resolved === null || resolved === head)
|
|
4834
|
+
continue;
|
|
4835
|
+
const relPaths = changedFiles(ref);
|
|
4836
|
+
if (relPaths === null)
|
|
4837
|
+
continue;
|
|
4838
|
+
return { base: ref, root, files: relPaths.map((p) => join2(root, p)) };
|
|
4839
|
+
}
|
|
4840
|
+
return null;
|
|
4841
|
+
}
|
|
4842
|
+
function changedLineRanges(ref, path) {
|
|
4843
|
+
const diff = fileDiffAgainst(ref, path);
|
|
4844
|
+
if (diff === null)
|
|
4845
|
+
return null;
|
|
4846
|
+
return parseChangedLineRanges(diff)[path] ?? null;
|
|
4847
|
+
}
|
|
4848
|
+
function lineIsInRanges(line, ranges) {
|
|
4849
|
+
return ranges.some(([start, end]) => line >= start && line <= end);
|
|
4850
|
+
}
|
|
4851
|
+
function findingsInThisDiff(findings) {
|
|
4852
|
+
const diff = resolveDiff();
|
|
4853
|
+
if (diff === null)
|
|
4854
|
+
return [...findings];
|
|
4855
|
+
const changed = new Set(diff.files);
|
|
4856
|
+
const ranges = new Map;
|
|
4857
|
+
return findings.filter((f) => {
|
|
4858
|
+
const abs = resolve2(diff.root, f.file);
|
|
4859
|
+
if (!changed.has(abs))
|
|
4860
|
+
return false;
|
|
4861
|
+
const relPath = relative(diff.root, abs);
|
|
4862
|
+
if (!ranges.has(relPath))
|
|
4863
|
+
ranges.set(relPath, changedLineRanges(diff.base, relPath));
|
|
4864
|
+
const fileRanges = ranges.get(relPath) ?? null;
|
|
4865
|
+
return fileRanges === null ? true : lineIsInRanges(f.line, fileRanges);
|
|
4866
|
+
});
|
|
4867
|
+
}
|
|
4868
|
+
|
|
4030
4869
|
// src/checks/bin/check-workspace-escape.ts
|
|
4031
4870
|
var CHECK_NAME = "workspace-escape";
|
|
4032
4871
|
var WORKSPACE_DIRS = ["apps", "packages"];
|
|
@@ -4042,7 +4881,7 @@ function collectAllPaths(dir, out = []) {
|
|
|
4042
4881
|
for (const name of entries) {
|
|
4043
4882
|
if (EXCLUDED_DIRS.has(name))
|
|
4044
4883
|
continue;
|
|
4045
|
-
const full =
|
|
4884
|
+
const full = join3(dir, name);
|
|
4046
4885
|
let isDir;
|
|
4047
4886
|
try {
|
|
4048
4887
|
isDir = statSync(full).isDirectory();
|
|
@@ -4058,10 +4897,11 @@ function collectAllPaths(dir, out = []) {
|
|
|
4058
4897
|
function main() {
|
|
4059
4898
|
const allPaths = WORKSPACE_DIRS.flatMap((d) => collectAllPaths(d)).map((p) => p.split("\\").join("/"));
|
|
4060
4899
|
const knownPaths = new Set(allPaths);
|
|
4061
|
-
const sourceFiles = allPaths.filter((p) => SOURCE_EXTENSIONS.some((ext) => p.endsWith(ext))).map((p) => ({ path: p, content:
|
|
4900
|
+
const sourceFiles = allPaths.filter((p) => SOURCE_EXTENSIONS.some((ext) => p.endsWith(ext))).map((p) => ({ path: p, content: readFileSync3(p, "utf8") }));
|
|
4062
4901
|
const findings = findWorkspaceEscapes(sourceFiles, knownPaths, WORKSPACE_DIRS);
|
|
4063
|
-
|
|
4064
|
-
|
|
4902
|
+
const reportable = findingsInThisDiff(findings);
|
|
4903
|
+
console.log(`${CHECK_NAME}: ${sourceFiles.length} source file(s) scanned under ${WORKSPACE_DIRS.join("/")}; ${findings.length} finding(s), ${reportable.length} in this diff`);
|
|
4904
|
+
for (const finding of reportable) {
|
|
4065
4905
|
const what = finding.reason === "escapes-workspace-package" ? `resolves to "${finding.resolved}", outside this file's own workspace package` : `resolves to "${finding.resolved}", which does not exist`;
|
|
4066
4906
|
emitCheckError({
|
|
4067
4907
|
schema: CHECK_SCHEMA_VERSION,
|