@arjunkhera/atlas 0.4.44 → 0.4.45

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.44",
3
+ "version": "0.4.45",
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.44",
3
+ "version": "0.4.45",
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",
package/shape/check.mjs CHANGED
@@ -38,7 +38,7 @@ export const sha256 = (text) => createHash('sha256').update(String(text ?? ''),
38
38
  // Whether a copy is current or behind is not decided here. CI has no package
39
39
  // and no network. The session verb decides it, from the release list that
40
40
  // ships beside this file in the package, shape/releases.json.
41
- export const CHECK_VERSION = 8;
41
+ export const CHECK_VERSION = 9;
42
42
 
43
43
  // ---------------------------------------------------------------------------
44
44
  // The spec. One object per version. The check reads a version it has here and
@@ -1550,6 +1550,33 @@ export function counts(files) {
1550
1550
  // check version 6 five cases switched the rule off and printed a warning that
1551
1551
  // a person merges the file. Nothing switches it off now. The base copy scores,
1552
1552
  // and the base kind and spec score both trees, so no case needs to.
1553
+ // A head whose atlas.yaml names a spec that this copy of the check cannot
1554
+ // read. A head with no spec, or with an atlas.yaml that does not read, is not
1555
+ // one: those stay errors.
1556
+ export function headSpecUnread(facts) {
1557
+ const spec = facts?.spec;
1558
+ return spec !== undefined && spec !== null && spec !== '' && !Object.hasOwn(SPECS, String(spec));
1559
+ }
1560
+
1561
+ // The head of a move to a newer spec: no part is scored, but every file the
1562
+ // newer spec can read for facts is swept for secrets. That is atlas.yaml, each
1563
+ // file under atlas/, and each facts file that atlas.yaml points to by a plain
1564
+ // path. The trust checks of the pattern list and the owner_only checks run as
1565
+ // in scan, so a newer spec cannot turn either of them from red into owner.
1566
+ export function sweepHead({ root, repoId = null, secretPatterns: patterns = secretPatterns() }) {
1567
+ const { facts } = readFacts(root);
1568
+ const errors = [...ownerOnlyErrors(facts)];
1569
+ const usable = Array.isArray(patterns) && patterns.every((pattern) => pattern instanceof RegExp) ? patterns : [];
1570
+ if (!usable.length) errors.push('the secret pattern list is empty or is not a list of patterns, so a credential value could not be caught; this check cannot be trusted');
1571
+ const missed = canaryMisses();
1572
+ if (missed.length) errors.push(`${missed.length} secret pattern(s) no longer catch their own canary: ${missed.join(', ')}; this check cannot be trusted`);
1573
+ const context = { root, secretPatterns: usable, errors };
1574
+ const files = facts?.files && typeof facts.files === 'object' && !Array.isArray(facts.files) ? facts.files : {};
1575
+ const pointed = Object.values(files).filter((path) => plainPath(path) && isFile(root, path));
1576
+ for (const path of new Set(['atlas.yaml', ...walk(root, 'atlas'), ...pointed])) secretIn(context, path, readText(root, path));
1577
+ return { ok: false, check_version: CHECK_VERSION, repo: repoId, kind: null, spec: facts?.spec ?? null, swept_only: true, files: [], by_reference: [], counts: counts([]), errors };
1578
+ }
1579
+
1553
1580
  export const OWNER_CASES = Object.freeze(['check-file', 'workflow', 'kind', 'spec', 'moved', 'first-run']);
1554
1581
 
1555
1582
  const fileHash = (root, path) => { const text = readText(root, path); return text === null ? null : sha256(text); };
@@ -1633,8 +1660,9 @@ export function noLoss({ headRoot, baseRoot, baseSha = null, baseCommit = null,
1633
1660
  if (String(out.head_facts.kind) !== String(scoreAs.kind)) {
1634
1661
  owner('kind', `atlas.yaml names kind "${scoreAs.kind}" at the base and "${out.head_facts.kind}" at the head; this pull request changes which parts the repo needs`);
1635
1662
  }
1663
+ const unread = Object.hasOwn(SPECS, String(scoreAs.spec)) && headSpecUnread(headRead.facts);
1636
1664
  if (String(out.head_facts.spec) !== String(scoreAs.spec)) {
1637
- owner('spec', `atlas.yaml names spec "${scoreAs.spec}" at the base and "${out.head_facts.spec}" at the head; this pull request changes what the spec version means`);
1665
+ owner('spec', `atlas.yaml names spec "${scoreAs.spec}" at the base and "${out.head_facts.spec}" at the head; this pull request changes what the spec version means${unread ? `; this copy of the check, check version ${CHECK_VERSION}, cannot read spec "${out.head_facts.spec}", so it did not score the head and only swept it for secrets` : ''}`);
1638
1666
  }
1639
1667
 
1640
1668
  // 3. Does the diff change the gate itself? The base copy judges, so the
@@ -1645,6 +1673,12 @@ export function noLoss({ headRoot, baseRoot, baseSha = null, baseCommit = null,
1645
1673
  }
1646
1674
  }
1647
1675
 
1676
+ // A head on a spec this copy cannot read is a move to a newer spec. This
1677
+ // copy has no yardstick for the head, so it compares nothing. The spec row
1678
+ // above sends it to the owner, and the gate still sweeps the head for
1679
+ // secrets (roadmap R62: the base copy judged the move to spec 3 red).
1680
+ if (unread) return out;
1681
+
1648
1682
  // 4. Score both trees, by the same yardstick, and compare part by part. The
1649
1683
  // head is scored twice: at its own pointers, and at the base's.
1650
1684
  const base = scan({ root: baseRoot, repoId, secretPatterns: patterns, scoreAs });
@@ -1988,7 +2022,9 @@ export function gateFromGit({ git, baseCommit, headCommit, repoId = null, trees
1988
2022
  }
1989
2023
  mkdirSync(baseRoot, { recursive: true });
1990
2024
  mkdirSync(headRoot, { recursive: true });
1991
- const report = scan({ root: headRoot, repoId, secretPatterns: patterns });
2025
+ const baseFacts = readFacts(baseRoot).facts;
2026
+ const swept = baseFacts && Object.hasOwn(SPECS, String(baseFacts.spec)) && headSpecUnread(readFacts(headRoot).facts);
2027
+ const report = swept ? sweepHead({ root: headRoot, repoId, secretPatterns: patterns }) : scan({ root: headRoot, repoId, secretPatterns: patterns });
1992
2028
  const state = noLoss({ headRoot, baseRoot, baseCommit: mergeBase, repoId, secretPatterns: patterns });
1993
2029
  // A link or a submodule that the head adds or changes is never read, so the
1994
2030
  // check cannot judge it. The owner reads it.
@@ -47,6 +47,11 @@
47
47
  "check_version": 8,
48
48
  "sha256": "8cf521f6e8f89ae65a257c521c583281e0c3c27ef968b4bd96eb336a8d095795",
49
49
  "workflow_sha256": "fcddebef030e6564f97e1a5fb81c8ad1b3084ebef9622167c6565698312dcdd7"
50
+ },
51
+ {
52
+ "check_version": 9,
53
+ "sha256": "87896abfc09afd22724cfa645bbb5ef029d83887caf70f38b88d00b21cddef02",
54
+ "workflow_sha256": "fcddebef030e6564f97e1a5fb81c8ad1b3084ebef9622167c6565698312dcdd7"
50
55
  }
51
56
  ]
52
57
  }