@arjunkhera/atlas 0.4.34 → 0.4.35

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas",
3
- "version": "0.4.34",
3
+ "version": "0.4.35",
4
4
  "description": "Atlas: the delivery lifecycle, its crews and the Atlas tools, for any repository.",
5
5
  "author": {
6
6
  "name": "Arjun Khera"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arjunkhera/atlas",
3
- "version": "0.4.34",
3
+ "version": "0.4.35",
4
4
  "description": "Atlas: the delivery lifecycle, its crews and the Atlas tools, as a Claude Code plugin for any repository.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -35,11 +35,18 @@ export const SECOND_CHECK = 'passes-after';
35
35
  // package.json or a runner config changes for many reasons, so it is not.
36
36
  const TEST_GROUPS = Object.freeze(['unit-test', 'check-block', 'scenario-words', 'test-helper', 'kit-manifest']);
37
37
  // The GitHub counts and the tracker counts of one row.
38
- export const GITHUB_COUNTS = Object.freeze(['merges', 'not_on_green', 'reverts', 'unchecked_test_changes', 'signed_acts']);
38
+ export const GITHUB_COUNTS = Object.freeze(['merges', 'not_on_green', 'reverts', 'unchecked_test_changes', 'signed_acts', 'audit_unsigned_merges']);
39
39
  export const GRAPH_COUNTS = Object.freeze(['reopens', 'step_ins', 'owner_messages', 'platform_messages']);
40
- // Line 2: the count that waits on an item not built yet. It is named, never
41
- // shown as zero.
42
- export const WAITING = Object.freeze({ audit_unsigned_merges: 'waits on R14 line 3, the unsigned-merge finding of the audit job' });
40
+ // Line 2: the counts that wait on an item not built yet. Each is named, never
41
+ // shown as zero. None waits now: R14 line 3 shipped the unsigned-merge finding.
42
+ export const WAITING = Object.freeze({});
43
+ // Line 2: the audit job (R14 line 3) opens one issue for each merge that the
44
+ // gate sent to the owner with no signed act. GitHub Actions writes it, with
45
+ // the label atlas-job and this title.
46
+ export const AUDIT_LABEL = 'atlas-job';
47
+ const AUDIT_TITLE = /^atlas-job: gate-audit owner-unsigned:([0-9a-f]{40})$/;
48
+ const AUDIT_AUTHOR = 'github-actions[bot]';
49
+ const MAX_ISSUE_PAGES = 10;
43
50
 
44
51
  const DATE = /^\d{4}-\d{2}-\d{2}$/;
45
52
  const dayOf = (value) => String(value ?? '').slice(0, 10);
@@ -108,6 +115,26 @@ async function readAllFiles(repo, number, readGitHub) {
108
115
  throw new Error(`pull request ${number} changed more than ${MAX_FILE_PAGES * PAGE} files`);
109
116
  }
110
117
 
118
+ // Line 2: the merge commits that the audit job reported as unsigned, from
119
+ // its issues opened since the start of the period. Only an issue that GitHub
120
+ // Actions opened counts, so a person cannot add one by hand. A closed issue
121
+ // still counts: the merge happened. More than MAX_ISSUE_PAGES pages is
122
+ // refused, never cut.
123
+ export async function readUnsignedMerges(repo, readGitHub, sinceAt) {
124
+ const shas = new Set();
125
+ const since = new Date(sinceAt).toISOString();
126
+ for (let page = 1; page <= MAX_ISSUE_PAGES; page += 1) {
127
+ const rows = await readGitHub(`repos/${repo}/issues?labels=${AUDIT_LABEL}&state=all&since=${since}&per_page=${PAGE}&page=${page}`);
128
+ if (!Array.isArray(rows)) throw new Error('the audit issues are not a list');
129
+ for (const row of rows) {
130
+ const match = AUDIT_TITLE.exec(String(row?.title ?? ''));
131
+ if (match && row?.user?.login === AUDIT_AUTHOR && !row.pull_request) shas.add(match[1].toLowerCase());
132
+ }
133
+ if (rows.length < PAGE) return shas;
134
+ }
135
+ throw new Error(`more than ${MAX_ISSUE_PAGES * PAGE} audit issues`);
136
+ }
137
+
111
138
  // Line 2: the comment texts of one pull request, every page. More than
112
139
  // MAX_COMMENT_PAGES pages is refused, never cut.
113
140
  export async function readAllComments(repo, number, readGitHub) {
@@ -222,6 +249,7 @@ export async function readMerges({ repo, defaultBranch, period, readGitHub }) {
222
249
  const text = await textAt('atlas.yaml', sha);
223
250
  return text === null ? [] : deliveryFacts(text).required;
224
251
  };
252
+ const unsigned = await readUnsignedMerges(repo, readGitHub, period.start);
225
253
  return pool(merges.map((pull) => async () => {
226
254
  const head = pull.head?.sha, merge = pull.merge_commit_sha;
227
255
  if (!/^[0-9a-f]{40}$/i.test(String(head)) || !/^[0-9a-f]{40}$/i.test(String(merge))) throw new Error(`GitHub names no head or merge commit for pull request ${pull.number}`);
@@ -245,6 +273,7 @@ export async function readMerges({ repo, defaultBranch, period, readGitHub }) {
245
273
  const signed = keys.length ? signedActOf({ repo, pull, texts: await readAllComments(repo, pull.number, readGitHub), keys }) : null;
246
274
  return {
247
275
  repo, number: pull.number, url: pull.html_url ?? `https://github.com/${repo}/pull/${pull.number}`, title: oneLine(pull.title ?? '', 200), merged_at: pull.merged_at,
276
+ audit_unsigned: unsigned.has(String(merge).toLowerCase()),
248
277
  not_on_green: notOnGreenReasons({ required, runs, mergedAt: pull.merged_at }), revert: isRevert(pull),
249
278
  changed_tests: tests, signed, unchecked: tests.length > 0 && !signed && !secondCheckPassed(runs, pull.merged_at),
250
279
  };
@@ -320,6 +349,7 @@ export async function readNumbers({ product, repos, items, marks, period, readGi
320
349
  if (merge.revert) events.push(['reverts', merge.merged_at]);
321
350
  if (merge.unchecked) events.push(['unchecked_test_changes', merge.merged_at]);
322
351
  if (merge.signed) events.push(['signed_acts', merge.merged_at]);
352
+ if (merge.audit_unsigned) events.push(['audit_unsigned_merges', merge.merged_at]);
323
353
  }
324
354
  for (const mark of reopens) if (itemRepos.get(mark.fields.item).includes(repo.id)) events.push(['reopens', mark.fields.marked_at]);
325
355
  for (const pair of pairs) {
@@ -341,6 +371,7 @@ export async function readNumbers({ product, repos, items, marks, period, readGi
341
371
  not_on_green: refused ? null : merges.filter((merge) => merge.not_on_green.length).map((merge) => ({ pr: merge.url, merged_at: merge.merged_at, reasons: merge.not_on_green })),
342
372
  reverts: refused ? null : merges.filter((merge) => merge.revert).map((merge) => ({ pr: merge.url, title: merge.title })),
343
373
  unchecked_test_changes: refused ? null : merges.filter((merge) => merge.unchecked).map((merge) => ({ pr: merge.url, tests: merge.changed_tests })),
374
+ audit_unsigned_merges: refused ? null : merges.filter((merge) => merge.audit_unsigned).map((merge) => ({ pr: merge.url, merged_at: merge.merged_at })),
344
375
  signed_acts: refused ? null : merges.filter((merge) => merge.signed).map((merge) => ({ pr: merge.url, signed_at: merge.signed.signed_at, key: merge.signed.key })),
345
376
  });
346
377
  }
@@ -266,7 +266,7 @@ function render(view) {
266
266
  }
267
267
 
268
268
  // Part 6. A refused count reads "refused", never 0.
269
- const COLUMNS = [['merges', 'merges'], ['not_on_green', 'not on green'], ['reverts', 'reverts'], ['unchecked_test_changes', 'unchecked test changes'], ['signed_acts', 'signed acts'], ['reopens', 'reopens'], ['step_ins', 'step-ins'], ['owner_messages', 'owner messages'], ['platform_messages', 'platform messages']];
269
+ const COLUMNS = [['merges', 'merges'], ['not_on_green', 'not on green'], ['reverts', 'reverts'], ['unchecked_test_changes', 'unchecked test changes'], ['signed_acts', 'signed acts'], ['audit_unsigned_merges', 'unsigned merges'], ['reopens', 'reopens'], ['step_ins', 'step-ins'], ['owner_messages', 'owner messages'], ['platform_messages', 'platform messages']];
270
270
  const countsLine = (row) => COLUMNS.map(([key, word]) => `${row[key] === null ? 'refused' : row[key]} ${word}`).join(' · ');
271
271
  function renderNumbers(numbers, more) {
272
272
  const out = [`6. The numbers, ${numbers.from} to ${numbers.to} (UTC)`];
@@ -284,7 +284,7 @@ function renderNumbers(numbers, more) {
284
284
  out.push(` input to live: ${numbers.delivered.length} delivered ${numbers.delivered.length === 1 ? 'item' : 'items'}${median === null ? '' : `, median ${median} days`}`);
285
285
  for (const row of numbers.delivered.slice(0, SHOWN)) out.push(listLine(row.title, `${row.item.slice(0, 8)} · ${row.days ?? '?'} days · live ${dayOf(row.live_at)}`, ' '));
286
286
  out.push(...more(numbers.delivered));
287
- out.push(` owner acts: unsigned merges ${numbers.owner_acts.audit_unsigned_merges}`);
287
+ for (const [count, wait] of Object.entries(numbers.owner_acts ?? {})) out.push(` ${count.replace(/_/g, ' ')}: ${wait}`);
288
288
  if (numbers.not_counted_sessions) out.push(` ${numbers.not_counted_sessions} sessions ran with the hooks off, so their messages were not counted`);
289
289
  return out.join('\n');
290
290
  }
package/work/mcp.mjs CHANGED
@@ -227,6 +227,9 @@ export const GITHUB_READS = Object.freeze([
227
227
  // and the owner key file at the first parent of the merge.
228
228
  new RegExp(`^repos/${REPO}/issues/\\d{1,9}/comments\\?per_page=\\d{1,3}&page=\\d{1,3}$`),
229
229
  new RegExp(`^repos/${REPO}/contents/atlas/owner-signers\\?ref=${SHA}$`),
230
+ // Roadmap R16 line 2: the audit issues of the period, for the merges that
231
+ // the audit job reported as unsigned.
232
+ new RegExp(`^repos/${REPO}/issues\\?labels=atlas-job&state=all&since=\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z&per_page=\\d{1,3}&page=\\d{1,3}$`),
230
233
  ]);
231
234
  export function githubReader() {
232
235
  return async (path) => {