@attalabs/vinaya 0.9.0 → 0.16.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.
Files changed (117) hide show
  1. package/README.md +33 -7
  2. package/aeg-root/contracts/planner-brief.md +1 -1
  3. package/aeg-root/enforcement.md +7 -7
  4. package/aeg-root/glossary.md +4 -0
  5. package/aeg-root/roles/archivist.md +2 -2
  6. package/aeg-root/roles/developer.md +29 -5
  7. package/aeg-root/roles/planner.md +3 -3
  8. package/aeg-root/roles/reviewer.md +17 -4
  9. package/aeg-root/roles/security.md +16 -5
  10. package/aeg-root/roles/tranche-archivist.md +1 -1
  11. package/aeg-root/skills/aeg/SKILL.md +1 -1
  12. package/aeg-root/state-machine.md +8 -8
  13. package/aeg-root/templates/pr-report-template.md +8 -4
  14. package/aeg-root/tranche-model.md +34 -12
  15. package/dist/checks/bin/check-branch-topology.js +232 -120
  16. package/dist/checks/bin/check-brief-shape.js +232 -120
  17. package/dist/checks/bin/check-closes-n.js +232 -120
  18. package/dist/checks/bin/check-coherence.js +234 -121
  19. package/dist/checks/bin/check-dead-branch-push.js +232 -120
  20. package/dist/checks/bin/check-dispatch-readiness.js +234 -121
  21. package/dist/checks/bin/check-doc-coverage-push.js +261 -130
  22. package/dist/checks/bin/check-doc-coverage.js +263 -132
  23. package/dist/checks/bin/check-evidence-fresh.js +3196 -0
  24. package/dist/checks/bin/check-first-push-dispatch.js +234 -121
  25. package/dist/checks/bin/check-issue-assignment.js +232 -120
  26. package/dist/checks/bin/check-no-disk-state.js +232 -120
  27. package/dist/checks/bin/check-reader-resolvable-prose.js +232 -120
  28. package/dist/checks/bin/check-registry-gates.js +232 -120
  29. package/dist/checks/bin/check-review-gate.js +237 -123
  30. package/dist/checks/bin/check-single-plan-pr.js +232 -120
  31. package/dist/checks/bin/check-test-plan.js +232 -120
  32. package/dist/index.js +1232 -262
  33. package/package.json +1 -1
  34. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  35. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  36. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  37. package/studio-standalone/apps/vinaya-studio/web/.next/required-server-files.json +3 -1
  38. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  39. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  40. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  41. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  42. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  43. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  44. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  45. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  46. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  47. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  48. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  49. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  50. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  51. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  52. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  53. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  54. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js +3 -3
  55. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  56. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  57. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  58. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  59. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  60. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  61. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js +2 -3
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  63. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  64. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  65. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  66. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  67. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  68. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  69. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  70. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  71. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  72. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  73. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__0e95dv5._.js +1 -1
  74. 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
  75. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__029-1ch._.js +1 -1
  76. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__05t6482._.js +1 -1
  77. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0gw1_g2._.js +1 -1
  78. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0mdkphe._.js +3 -0
  79. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0plfbyn._.js +45 -0
  80. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0q56ohl._.js +1 -1
  81. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0rr_lq7._.js +51 -0
  82. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0zfjc2o._.js +1 -1
  83. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__19jyp3m._.js +1 -1
  84. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1_ojzp1._.js +1 -1
  85. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1d_8nm4._.js +1 -1
  86. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1p19382._.js → [root-of-the-server]__1qpl4m_._.js} +1 -1
  87. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1zkwtx8._.js +1 -1
  88. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__210ehg9._.js +1 -1
  89. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_00yjcv1._.js +1 -1
  90. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0o54suy._.js +1 -1
  91. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0rix02i._.js +1 -1
  92. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_18-d2be._.js +1 -1
  93. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +5 -4
  94. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1gw6vc9._.js +1 -1
  95. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_20yap_f._.js +1 -1
  96. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  97. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  98. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  99. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  100. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  101. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3antrj5z0uf_c.js → 0ek6qlqhokys0.js} +1 -1
  102. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1khy4oilrab6g.js → 0fm4p9os2lsom.js} +1 -1
  103. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3zyedaek9nvr_.js → 0ywkq74falx-x.js} +1 -1
  104. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1s9ixp5nhnevp.js +1 -0
  105. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/27sqffqm2wbxx.css +1 -0
  106. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  107. package/studio-standalone/apps/vinaya-studio/web/server.js +1 -1
  108. package/templates/custom-check.template.ts +5 -0
  109. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1s7kj3s._.js +0 -3
  110. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1tswrm4._.js +0 -9
  111. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_01jl99k._.js +0 -45
  112. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_20c6-3m._.js +0 -45
  113. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0qu0xgvifbacu.css +0 -1
  114. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/2e68sbtdhi5dg.js +0 -1
  115. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → R7xvB2iuNFp_jxENpL7xv}/_buildManifest.js +0 -0
  116. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → R7xvB2iuNFp_jxENpL7xv}/_clientMiddlewareManifest.js +0 -0
  117. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{pY8RKWuVAUZOjj2-bAYCY → R7xvB2iuNFp_jxENpL7xv}/_ssgManifest.js +0 -0
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // ../../packages/aeg-core/src/anchored-region.ts
2
+ // ../../packages/aeg-forge-state/src/strip-code.ts
3
3
  function maskCode(body) {
4
4
  const fill = (line) => " ".repeat(line.length);
5
5
  return maskIndentedCode(maskFencedCode(body, fill), fill).replace(/(`+)[^\n]*?\1/g, (m) => " ".repeat(m.length));
@@ -16,29 +16,38 @@ function stripCode(body, options = {}) {
16
16
  var FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})([^\r\n]*)\r?$/;
17
17
  var FENCE_CLOSE = /^ {0,3}(`+|~+)[ \t]*\r?$/;
18
18
  function maskFencedCode(body, fill) {
19
+ const { lines } = scanFencedCode(body);
20
+ return lines.map(({ line, inCode }) => inCode ? fill(line) : line).join(`
21
+ `);
22
+ }
23
+ function scanFencedCode(body) {
19
24
  let fenceChar = null;
20
25
  let fenceLen = 0;
21
- return body.split(`
26
+ const lines = body.split(`
22
27
  `).map((line) => {
23
28
  if (fenceChar === null) {
24
29
  const open = line.match(FENCE_OPEN);
25
30
  if (!open)
26
- return line;
31
+ return { line, inCode: false };
27
32
  const marker = open[1];
28
33
  if (marker[0] === "`" && open[2].includes("`"))
29
- return line;
34
+ return { line, inCode: false };
30
35
  fenceChar = marker[0];
31
36
  fenceLen = marker.length;
32
- return fill(line);
37
+ return { line, inCode: true };
33
38
  }
34
39
  const close = line.match(FENCE_CLOSE);
35
40
  if (close && close[1][0] === fenceChar && close[1].length >= fenceLen) {
36
41
  fenceChar = null;
37
42
  fenceLen = 0;
38
43
  }
39
- return fill(line);
40
- }).join(`
41
- `);
44
+ return { line, inCode: true };
45
+ });
46
+ return { lines, unterminated: fenceChar !== null };
47
+ }
48
+ function hasUnterminatedFence(body) {
49
+ return scanFencedCode(body.replace(/\r\n?/g, `
50
+ `)).unterminated;
42
51
  }
43
52
  var LIST_MARKER = /^ {0,3}(?:[-*+]|\d{1,9}[.)])(?:\s|$)/;
44
53
  function indentWidth(line) {
@@ -81,88 +90,7 @@ function maskIndentedCode(body, fill) {
81
90
  return out.join(`
82
91
  `);
83
92
  }
84
- function anchoredRegion(body, field) {
85
- const masked = maskCode(body);
86
- const start = new RegExp(`<!--\\s*AEG:${field}:START\\s*-->`).exec(masked);
87
- if (!start)
88
- return null;
89
- const afterStart = start.index + start[0].length;
90
- const end = new RegExp(`<!--\\s*AEG:${field}:END\\s*-->`).exec(masked.slice(afterStart));
91
- if (!end)
92
- return null;
93
- return body.slice(afterStart, afterStart + end.index);
94
- }
95
- // ../../packages/aeg-core/src/parse-registry.ts
96
- function parseRegistry(md) {
97
- const rows = readMarkdownTable(md, /^##\s+Registry\b/i);
98
- const projects = [];
99
- for (const row of rows) {
100
- if (row.length < 4)
101
- continue;
102
- const name = stripBackticks(row[0] ?? "").trim();
103
- const path = stripBackticks(row[1] ?? "").trim();
104
- const specsPath = stripBackticks(row[2] ?? "").trim();
105
- const stateCell = (row[3] ?? "").trim();
106
- if (!name || !path || !specsPath)
107
- continue;
108
- projects.push({
109
- name,
110
- path,
111
- specsPath,
112
- statePath: parseStateCell(stateCell)
113
- });
114
- }
115
- return projects;
116
- }
117
- function parseStateCell(cell) {
118
- if (!cell)
119
- return null;
120
- const stripped = stripBackticks(cell).trim();
121
- if (/^[\w./@-]+\/?$/.test(stripped) && cell.includes("`")) {
122
- return stripped;
123
- }
124
- return null;
125
- }
126
- function stripBackticks(s) {
127
- return s.replace(/`/g, "");
128
- }
129
- function readMarkdownTable(md, headingMatcher) {
130
- const lines = md.split(/\r?\n/);
131
- let i = 0;
132
- for (;i < lines.length; i++) {
133
- if (headingMatcher.test(lines[i] ?? "")) {
134
- i++;
135
- break;
136
- }
137
- }
138
- if (i >= lines.length)
139
- return [];
140
- for (;i < lines.length; i++) {
141
- if ((lines[i] ?? "").trim().startsWith("|"))
142
- break;
143
- }
144
- if (i + 1 >= lines.length)
145
- return [];
146
- i += 2;
147
- const out = [];
148
- for (;i < lines.length; i++) {
149
- const trimmed = (lines[i] ?? "").trim();
150
- if (!trimmed.startsWith("|"))
151
- break;
152
- out.push(splitTableRow(trimmed));
153
- }
154
- return out;
155
- }
156
- function splitTableRow(row) {
157
- const PLACEHOLDER = "\x00PIPE\x00";
158
- const escaped = row.replace(/\\\|/g, PLACEHOLDER);
159
- const parts = escaped.split("|").map((s) => s.trim());
160
- if (parts.length && parts[0] === "")
161
- parts.shift();
162
- if (parts.length && parts[parts.length - 1] === "")
163
- parts.pop();
164
- return parts.map((p) => p.split(PLACEHOLDER).join("|"));
165
- }
93
+
166
94
  // ../../packages/aeg-forge-state/src/parse-rationale-deps.ts
167
95
  var SECTION_HEADER = /\*\*Dependency rationale\*\*/i;
168
96
  var NEXT_HEADER = /\*\*[A-Z][^*]*\*\*/;
@@ -460,11 +388,37 @@ function findTrancheSlug(names) {
460
388
  var TITLE_PATTERN = /^\[([^\]]+)]\s*(\S+)\s*—\s*(.+)$/;
461
389
  var PROJECT_SLUG = /^[a-z0-9][a-z0-9-]*$/i;
462
390
  var PROJECT_FIELD = /^\s*(?:\*\*)?Project(?:\(s\))?(?:\*\*)?\s*:\s*(?:\*\*)?\s*(.+)$/im;
391
+ var VALUE_EDGE_MARKUP = /^[`.;\s]+|[`.;\s]+$/g;
392
+ function unwrapValue(raw) {
393
+ return raw.replace(/\*\*/g, "").replace(VALUE_EDGE_MARKUP, "");
394
+ }
395
+ function projectFieldFromBody(body) {
396
+ const m = stripCode(body, { inlineSpans: "keep" }).match(PROJECT_FIELD);
397
+ if (m)
398
+ return parseFieldValue(m[1] ?? "");
399
+ if (hasUnterminatedFence(body)) {
400
+ const raw = body.match(PROJECT_FIELD);
401
+ if (raw)
402
+ return { declared: true, names: [], unparsed: [(raw[1] ?? "").trim()], unreadable: true };
403
+ }
404
+ return { declared: false, names: [], unparsed: [], unreadable: false };
405
+ }
406
+ function parseFieldValue(value) {
407
+ const names = [];
408
+ const unparsed = [];
409
+ for (const raw of value.split(",")) {
410
+ const name = unwrapValue(raw);
411
+ if (name.length === 0)
412
+ continue;
413
+ if (PROJECT_SLUG.test(name))
414
+ names.push(name);
415
+ else
416
+ unparsed.push(raw.trim());
417
+ }
418
+ return { declared: true, names: [...new Set(names)], unparsed, unreadable: false };
419
+ }
463
420
  function projectsFromBody(body) {
464
- const m = body.match(PROJECT_FIELD);
465
- if (!m)
466
- return [];
467
- return (m[1] ?? "").split(",").map((s) => s.trim()).filter((s) => PROJECT_SLUG.test(s));
421
+ return projectFieldFromBody(body).names;
468
422
  }
469
423
  function taskFromIssue(issue) {
470
424
  const m = issue.title.match(TITLE_PATTERN);
@@ -506,8 +460,8 @@ function tasksFromIssues(issues) {
506
460
  }
507
461
  return tasks.sort((a, b) => compareTaskIds(a.id, b.id));
508
462
  }
509
- async function listTasksForSlugAsync(owner, repo, slug) {
510
- return tasksFromIssues(await ghIssueListByAnyLabelAsync(owner, repo, [trancheLabel(slug)]));
463
+ async function fetchTrancheIssuesAsync(owner, repo, slug) {
464
+ return ghIssueListByAnyLabelAsync(owner, repo, [trancheLabel(slug)]);
511
465
  }
512
466
  function resolveTaskIssueRef(title, labels) {
513
467
  const m = title.match(TITLE_PATTERN);
@@ -523,17 +477,20 @@ function resolveTaskIssueRef(title, labels) {
523
477
  }
524
478
 
525
479
  // ../../packages/aeg-forge-state/src/derive-from-forge.ts
526
- async function deriveTrancheFromForge(owner, repo, slug, known) {
527
- const milestone = known ?? findMilestoneForSlug(owner, repo, slug);
528
- const tasks = await listTasksForSlugAsync(owner, repo, slug);
480
+ function trancheFromIssues(slug, issues, known) {
529
481
  return {
530
482
  name: slug,
531
- lifecycle: milestone?.lifecycle ?? "active",
532
- goal: milestone?.goal ?? "",
533
- tasks,
483
+ lifecycle: known?.lifecycle ?? "active",
484
+ goal: known?.goal ?? "",
485
+ tasks: tasksFromIssues(issues),
534
486
  backlog: []
535
487
  };
536
488
  }
489
+ async function deriveTrancheFromForge(owner, repo, slug, known) {
490
+ const milestone = known ?? findMilestoneForSlug(owner, repo, slug);
491
+ const issues = await fetchTrancheIssuesAsync(owner, repo, slug);
492
+ return trancheFromIssues(slug, issues, milestone);
493
+ }
537
494
  // ../../node_modules/universal-user-agent/index.js
538
495
  function getUserAgent() {
539
496
  if (typeof navigator === "object" && "userAgent" in navigator) {
@@ -1453,6 +1410,96 @@ var systemEnv3 = {
1453
1410
  ...process.env,
1454
1411
  PATH: [process.env.PATH, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].filter(Boolean).join(":")
1455
1412
  };
1413
+ // ../../packages/aeg-core/src/anchored-region.ts
1414
+ function anchoredRegionBounds(body, field) {
1415
+ const masked = maskCode(body);
1416
+ const start = new RegExp(`<!--\\s*AEG:${field}:START\\s*-->`).exec(masked);
1417
+ if (!start)
1418
+ return null;
1419
+ const innerStart = start.index + start[0].length;
1420
+ const end = new RegExp(`<!--\\s*AEG:${field}:END\\s*-->`).exec(masked.slice(innerStart));
1421
+ if (!end)
1422
+ return null;
1423
+ const innerEnd = innerStart + end.index;
1424
+ return { outerStart: start.index, outerEnd: innerEnd + end[0].length, innerStart, innerEnd };
1425
+ }
1426
+ function anchoredRegion(body, field) {
1427
+ const bounds = anchoredRegionBounds(body, field);
1428
+ if (!bounds)
1429
+ return null;
1430
+ return body.slice(bounds.innerStart, bounds.innerEnd);
1431
+ }
1432
+ // ../../packages/aeg-core/src/parse-registry.ts
1433
+ function parseRegistry(md) {
1434
+ const rows = readMarkdownTable(md, /^##\s+Registry\b/i);
1435
+ const projects = [];
1436
+ for (const row of rows) {
1437
+ if (row.length < 4)
1438
+ continue;
1439
+ const name = stripBackticks(row[0] ?? "").trim();
1440
+ const path = stripBackticks(row[1] ?? "").trim();
1441
+ const specsPath = stripBackticks(row[2] ?? "").trim();
1442
+ const stateCell = (row[3] ?? "").trim();
1443
+ if (!name || !path || !specsPath)
1444
+ continue;
1445
+ projects.push({
1446
+ name,
1447
+ path,
1448
+ specsPath,
1449
+ statePath: parseStateCell(stateCell)
1450
+ });
1451
+ }
1452
+ return projects;
1453
+ }
1454
+ function parseStateCell(cell) {
1455
+ if (!cell)
1456
+ return null;
1457
+ const stripped = stripBackticks(cell).trim();
1458
+ if (/^[\w./@-]+\/?$/.test(stripped) && cell.includes("`")) {
1459
+ return stripped;
1460
+ }
1461
+ return null;
1462
+ }
1463
+ function stripBackticks(s) {
1464
+ return s.replace(/`/g, "");
1465
+ }
1466
+ function readMarkdownTable(md, headingMatcher) {
1467
+ const lines = md.split(/\r?\n/);
1468
+ let i = 0;
1469
+ for (;i < lines.length; i++) {
1470
+ if (headingMatcher.test(lines[i] ?? "")) {
1471
+ i++;
1472
+ break;
1473
+ }
1474
+ }
1475
+ if (i >= lines.length)
1476
+ return [];
1477
+ for (;i < lines.length; i++) {
1478
+ if ((lines[i] ?? "").trim().startsWith("|"))
1479
+ break;
1480
+ }
1481
+ if (i + 1 >= lines.length)
1482
+ return [];
1483
+ i += 2;
1484
+ const out = [];
1485
+ for (;i < lines.length; i++) {
1486
+ const trimmed = (lines[i] ?? "").trim();
1487
+ if (!trimmed.startsWith("|"))
1488
+ break;
1489
+ out.push(splitTableRow(trimmed));
1490
+ }
1491
+ return out;
1492
+ }
1493
+ function splitTableRow(row) {
1494
+ const PLACEHOLDER = "\x00PIPE\x00";
1495
+ const escaped = row.replace(/\\\|/g, PLACEHOLDER);
1496
+ const parts = escaped.split("|").map((s) => s.trim());
1497
+ if (parts.length && parts[0] === "")
1498
+ parts.shift();
1499
+ if (parts.length && parts[parts.length - 1] === "")
1500
+ parts.pop();
1501
+ return parts.map((p) => p.split(PLACEHOLDER).join("|"));
1502
+ }
1456
1503
  // ../../packages/aeg-core/src/state-machine-model.ts
1457
1504
  var FORGE_FACT_INPUTS = [
1458
1505
  {
@@ -1956,23 +2003,65 @@ function checkIssueRationale(body) {
1956
2003
  function isTaskIssueLabelSet(labels) {
1957
2004
  return hasLabel("tranche", labels);
1958
2005
  }
2006
+ function isControlCodePoint(codePoint) {
2007
+ return codePoint <= 31 || codePoint >= 127 && codePoint <= 159;
2008
+ }
2009
+ function isDisplayControlCodePoint(codePoint) {
2010
+ return codePoint >= 8203 && codePoint <= 8207 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8294 && codePoint <= 8297 || codePoint === 65279;
2011
+ }
2012
+ var MESSAGE_VALUE_COUNT_MAX = 5;
2013
+ function residueForMessage(values) {
2014
+ const shown = values.slice(0, MESSAGE_VALUE_COUNT_MAX).map((v) => `\`${forMessage(v)}\``);
2015
+ const rest = values.length - shown.length;
2016
+ return rest > 0 ? `${shown.join(", ")} and ${rest} more` : shown.join(", ");
2017
+ }
2018
+ var MESSAGE_VALUE_MAX = 64;
2019
+ function forMessage(value) {
2020
+ const kept = [];
2021
+ for (const ch of value.replace(/\s+/g, " ")) {
2022
+ const codePoint = ch.codePointAt(0) ?? 0;
2023
+ if (isControlCodePoint(codePoint) || isDisplayControlCodePoint(codePoint))
2024
+ continue;
2025
+ kept.push(ch);
2026
+ }
2027
+ const cleaned = kept.join("").trim();
2028
+ if (cleaned.length === 0)
2029
+ return "(unprintable)";
2030
+ const points = Array.from(cleaned);
2031
+ return points.length > MESSAGE_VALUE_MAX ? `${points.slice(0, MESSAGE_VALUE_MAX).join("")}…` : cleaned;
2032
+ }
1959
2033
  function checkProjectsRegistered(body, _labels, registeredNames) {
1960
2034
  if (registeredNames.length === 0)
1961
2035
  return { status: "pass", errors: [] };
1962
- const known = new Set(registeredNames.map((n) => n.trim()));
1963
- const unregistered = projectsFromBody(body).filter((p) => !known.has(p));
1964
- if (unregistered.length === 0)
2036
+ const field = projectFieldFromBody(body);
2037
+ if (!field.declared)
1965
2038
  return { status: "pass", errors: [] };
1966
- const caseHints = unregistered.map((p) => {
1967
- const row = [...known].find((k) => k.toLowerCase() === p.toLowerCase());
1968
- return row ? `\`${p}\` differs from the registered \`${row}\` only in case` : null;
1969
- }).filter((h) => h !== null);
1970
- return {
1971
- status: "fail",
1972
- errors: [
1973
- `issue-validation project registry: the \`**Project:**\` field declares ${unregistered.join(", ")} no such row in \`.vinaya/projects.md\`, which is the authority for valid project names (registered: ${[...registeredNames].join(", ")}). Fix the name, or register the project with \`vinaya init product <name> --path <folder>\` first; an unregistered project has no specs to read and no per-project state to update.${caseHints.length > 0 ? ` Note: ${caseHints.join("; ")} project names are matched exactly, because every downstream consumer compares them literally.` : ""} This reads the same field \`projectsFromBody\` derives the task's project from, so a name here that is not a row is a task that resolves to a project that does not exist.`
1974
- ]
1975
- };
2039
+ const known = new Set(registeredNames.map((n) => n.trim()));
2040
+ const registeredList = [...registeredNames].map(forMessage).join(", ");
2041
+ const errors = [];
2042
+ if (field.unreadable) {
2043
+ return {
2044
+ status: "fail",
2045
+ errors: [
2046
+ `issue-validation project registry: the body has an unterminated code fence, so its \`**Project:**\` field could not be read reliably the fence runs to end of body (CommonMark, and how the forge renders it), swallowing everything after it including the foot field where the declaration lives by convention. Read from the raw body the field says ${residueForMessage(field.unparsed) || "(nothing)"}, but a line inside a swallowed region may be a quoted example rather than a real declaration, so this gate refuses rather than guessing which. Balance the fences every opening run of backticks or tildes needs a closing run at least as long and the field reads normally.`
2047
+ ]
2048
+ };
2049
+ }
2050
+ const unregistered = field.names.filter((p) => !known.has(p));
2051
+ if (unregistered.length > 0) {
2052
+ const caseHints = unregistered.map((p) => {
2053
+ const row = [...known].find((k) => k.toLowerCase() === p.toLowerCase());
2054
+ return row ? `\`${forMessage(p)}\` differs from the registered \`${forMessage(row)}\` only in case` : null;
2055
+ }).filter((h) => h !== null);
2056
+ errors.push(`issue-validation project registry: the \`**Project:**\` field declares ${unregistered.map(forMessage).join(", ")} — no such row in \`.vinaya/projects.md\`, which is the authority for valid project names (registered: ${registeredList}). Fix the name, or register the project with \`vinaya init product <name> --path <folder>\` first; an unregistered project has no specs to read and no per-project state to update.${caseHints.length > 0 ? ` Note: ${caseHints.join("; ")} — project names are matched exactly, because every downstream consumer compares them literally.` : ""} This reads the same field \`projectsFromBody\` derives the task's project from, so a name here that is not a row is a task that resolves to a project that does not exist.`);
2057
+ }
2058
+ if (field.names.length === 0) {
2059
+ const residue = field.unparsed.length > 0 ? ` — ${residueForMessage(field.unparsed)}` : " — the field is empty";
2060
+ errors.push(`issue-validation project registry: the \`**Project:**\` field is present but resolves to no project name${residue} (registered: ${registeredList}). A project name is a slug (\`[a-z0-9][a-z0-9-]*\`, matched exactly); prose, a parenthetical, or a sentence in this field resolves to no project at all, and this gate cannot check a name it never receives — which is how a declaration like this used to pass. Write the registered name on its own, or register the project with \`vinaya init product <name> --path <folder>\` first. If the task genuinely touches no registered project, omit the field rather than explaining its absence inside it — \`checkIssueRationale\` already requires the \`Project(s) + blast radius\` narrative field for that.`);
2061
+ } else if (field.unparsed.length > 0) {
2062
+ errors.push(`issue-validation project registry: the \`**Project:**\` field declares a value that is not a project name — ${residueForMessage(field.unparsed)} (registered: ${registeredList}). The rest of the field parsed, so this is a name the gate silently could not check rather than a field it could not read at all. A project name is a slug (\`[a-z0-9][a-z0-9-]*\`, matched exactly); write it on its own, or drop it if it names no project.`);
2063
+ }
2064
+ return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
1976
2065
  }
1977
2066
 
1978
2067
  // ../../packages/aeg-core/src/coherence-checks.ts
@@ -2276,15 +2365,25 @@ function checkClosesN2(branch, prBody, trancheFiles, taskIssueRefs) {
2276
2365
  return { ok: true, expectedIssue };
2277
2366
  }
2278
2367
  // ../../packages/aeg-core/src/verdict-extraction.ts
2368
+ var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
2369
+ function extractHeadSha(comment) {
2370
+ const m = comment.match(HEAD_SHA_PATTERN);
2371
+ return m ? m[1].toLowerCase() : null;
2372
+ }
2279
2373
  function extractVerdict(comments, valuePattern, missingLabel) {
2280
2374
  const clearHits = comments.filter((c) => valuePattern.test(c));
2281
2375
  if (clearHits.length > 0) {
2282
2376
  const latest = clearHits[clearHits.length - 1];
2283
2377
  const m = latest.match(valuePattern);
2284
- return { value: m[1].toUpperCase().replace(/[_-]/g, " "), danglingNote: null };
2378
+ return {
2379
+ value: m[1].toUpperCase().replace(/[_-]/g, " "),
2380
+ headSha: extractHeadSha(latest),
2381
+ danglingNote: null
2382
+ };
2285
2383
  }
2286
2384
  return {
2287
2385
  value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
2386
+ headSha: null,
2288
2387
  danglingNote: `no ${missingLabel} verdict comment found on this PR`
2289
2388
  };
2290
2389
  }
@@ -2298,6 +2397,11 @@ function extractSecurityReviewVerdict(comments) {
2298
2397
  function isReviewGateExemptBranch(branch) {
2299
2398
  return branch.startsWith("plan/");
2300
2399
  }
2400
+ function isBoundToHead(extraction, headSha) {
2401
+ if (!extraction.headSha)
2402
+ return false;
2403
+ return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
2404
+ }
2301
2405
  function checkReviewGate(input) {
2302
2406
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
2303
2407
  const waived = isWaiverLabelActorVerified({
@@ -2320,18 +2424,26 @@ function checkReviewGate(input) {
2320
2424
  const security = extractSecurityReviewVerdict(verifiedBodies);
2321
2425
  const codeReviewClean = codeReview.value === "APPROVE";
2322
2426
  const securityClean = security.value === "PASS";
2323
- if (codeReviewClean && securityClean) {
2427
+ const codeReviewBound = isBoundToHead(codeReview, input.headSha);
2428
+ const securityBound = isBoundToHead(security, input.headSha);
2429
+ if (codeReviewClean && codeReviewBound && securityClean && securityBound) {
2324
2430
  return {
2325
2431
  verdict: "pass",
2326
- reason: "code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS.",
2432
+ reason: `code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS, both covering head ${input.headSha}.`,
2327
2433
  waived: false
2328
2434
  };
2329
2435
  }
2330
2436
  const problems = [];
2331
- if (!codeReviewClean)
2437
+ if (!codeReviewClean) {
2332
2438
  problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
2333
- if (!securityClean)
2439
+ } else if (!codeReviewBound) {
2440
+ problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
2441
+ }
2442
+ if (!securityClean) {
2334
2443
  problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
2444
+ } else if (!securityBound) {
2445
+ problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
2446
+ }
2335
2447
  const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
2336
2448
  return {
2337
2449
  verdict: "fail",