@arjunkhera/atlas 0.3.46 → 0.3.49

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arjunkhera/atlas",
3
- "version": "0.3.46",
3
+ "version": "0.3.49",
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",
@@ -26,7 +26,8 @@
26
26
  "agents/",
27
27
  "hooks/",
28
28
  ".claude-plugin/",
29
- ".mcp.json"
29
+ ".mcp.json",
30
+ "owner-only.yaml"
30
31
  ],
31
32
  "peerDependencies": {
32
33
  "playwright": ">=1.40.0"
package/shape/check.mjs CHANGED
@@ -34,7 +34,7 @@ export const sha256 = (text) => createHash('sha256').update(String(text ?? ''),
34
34
  // Whether a copy is current or behind is not decided here. CI has no package
35
35
  // and no network. The session verb decides it, from the release list that
36
36
  // ships beside this file in the package, shape/releases.json.
37
- export const CHECK_VERSION = 4;
37
+ export const CHECK_VERSION = 5;
38
38
 
39
39
  // ---------------------------------------------------------------------------
40
40
  // The spec. One object per version. The check reads a version it has here and
@@ -774,7 +774,11 @@ function indentOf(line) {
774
774
  return line.length - line.trimStart().length;
775
775
  }
776
776
 
777
- export function parseYaml(source) {
777
+ // `strict` is for atlas.yaml. It refuses a key that appears twice, an anchor,
778
+ // an alias and a merge key. Each one lets two readers see two different maps
779
+ // in one file, and the owner-only reader and this check must see the same one
780
+ // (security review of the owner-only list, 24 October 2026).
781
+ export function parseYaml(source, { strict = false } = {}) {
778
782
  const raw = String(source ?? '').replace(/\r\n?/g, '\n').split('\n');
779
783
  const lines = [];
780
784
  for (let index = 0; index < raw.length; index += 1) {
@@ -825,6 +829,7 @@ export function parseYaml(source) {
825
829
  const body = line.text.trimStart();
826
830
  if (!body.startsWith('- ') && body !== '-') break;
827
831
  const rest = body === '-' ? '' : body.slice(2);
832
+ if (strict && /^[&*]/.test(rest.trim())) throw new YamlError('an anchor or an alias is not read in strict mode', line.number, line.indent);
828
833
  state.at += 1;
829
834
  if (!rest.trim()) { out.push(value(indent + 1)); continue; }
830
835
  const match = KEY.exec(rest);
@@ -851,6 +856,11 @@ export function parseYaml(source) {
851
856
  if (!match) throw new YamlError('this line is neither a key nor a list entry', line.number, line.indent);
852
857
  const key = match[1] !== undefined ? match[1] : match[2] !== undefined ? match[2].replaceAll("''", "'") : match[3].trim();
853
858
  const rest = (match[4] ?? '').trim();
859
+ if (strict) {
860
+ if (Object.hasOwn(out, key)) throw new YamlError(`the key "${key}" appears twice`, line.number, line.indent);
861
+ if (key === '<<' || /^[&*]/.test(key)) throw new YamlError('a merge key, an anchor or an alias is not read in strict mode', line.number, line.indent);
862
+ if (/^[&*]/.test(rest)) throw new YamlError('an anchor or an alias is not read in strict mode', line.number, line.indent);
863
+ }
854
864
  state.at += 1;
855
865
  if (/^[|>][-+]?\d*$/.test(rest)) { out[key] = blockScalar(rest, indent); continue; }
856
866
  if (rest && !rest.startsWith('#')) { out[key] = scalar(rest, line.number); continue; }
@@ -1428,11 +1438,41 @@ function byReference(context, spec) {
1428
1438
  return applyExemption([...found.values()].sort((a, b) => a.path.localeCompare(b.path)));
1429
1439
  }
1430
1440
 
1441
+ // The eight classes of the owner-only list. The authority is the package's
1442
+ // owner-only.yaml, and a test keeps the two equal. This copy exists because the
1443
+ // check runs alone in each repo's CI, where the package is not there.
1444
+ export const OWNER_ONLY_CLASSES = Object.freeze(['money', 'credentials', 'outside-visibility', 'lost-data', 'security', 'new-service', 'rule-book', 'direction']);
1445
+
1446
+ const stringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === 'string' && entry.trim() !== '');
1447
+
1448
+ // The three keys of the owner-only list in atlas.yaml. Each is optional. When
1449
+ // one is there, its shape is checked, and an unknown class is an error.
1450
+ export function ownerOnlyErrors(facts) {
1451
+ const errors = [];
1452
+ if (!facts || typeof facts !== 'object') return errors;
1453
+ if (Object.hasOwn(facts, 'owner_only')) {
1454
+ const list = facts.owner_only;
1455
+ if (!Array.isArray(list)) errors.push('atlas.yaml: owner_only is not a list');
1456
+ else list.forEach((entry, index) => {
1457
+ const at = `atlas.yaml: owner_only[${index}]`;
1458
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) { errors.push(`${at} is not a map with a path and a class`); return; }
1459
+ for (const key of Object.keys(entry)) if (!['path', 'class', 'why', 'except'].includes(key)) errors.push(`${at} has the key "${key}"; the keys are path, class, why and except`);
1460
+ if (typeof entry.path !== 'string' || !entry.path.trim()) errors.push(`${at} has no path`);
1461
+ if (!OWNER_ONLY_CLASSES.includes(entry.class)) errors.push(`${at}.class "${entry.class}" is not a class of owner-only.yaml; the classes are ${OWNER_ONLY_CLASSES.join(', ')}`);
1462
+ if (entry.except !== undefined && !stringList(entry.except)) errors.push(`${at}.except is not a list of paths`);
1463
+ });
1464
+ }
1465
+ for (const key of ['required_checks', 'product_paths']) {
1466
+ if (Object.hasOwn(facts, key) && !stringList(facts[key])) errors.push(`atlas.yaml: ${key} is not a list of names`);
1467
+ }
1468
+ return errors;
1469
+ }
1470
+
1431
1471
  export function readFacts(root) {
1432
1472
  const text = readText(root, 'atlas.yaml');
1433
1473
  if (text === null || !text.trim()) return { facts: null, error: null };
1434
1474
  try {
1435
- const data = parseYaml(text);
1475
+ const data = parseYaml(text, { strict: true });
1436
1476
  if (!data || typeof data !== 'object' || Array.isArray(data)) return { facts: null, error: 'atlas.yaml is not a map of keys' };
1437
1477
  return { facts: data, error: null };
1438
1478
  } catch (error) { return { facts: null, error: `atlas.yaml cannot be read: ${error.message}` }; }
@@ -1460,6 +1500,7 @@ export function scan({ root, repoId = null, extractors = [], secretPatterns: pat
1460
1500
  if (missed.length) errors.push(`${missed.length} secret pattern(s) no longer catch their own canary: ${missed.join(', ')}; this check cannot be trusted`);
1461
1501
  if (!facts && !factsError) errors.push('atlas.yaml does not exist, so the check cannot learn the spec version or the repo kind');
1462
1502
  if (factsError) errors.push(factsError);
1503
+ errors.push(...ownerOnlyErrors(facts));
1463
1504
  const kind = scoreAs ? scoreAs.kind ?? null : facts?.kind ?? null;
1464
1505
  const wantedSpec = scoreAs ? scoreAs.spec : facts?.spec;
1465
1506
  let spec = null;
@@ -20,6 +20,11 @@
20
20
  "check_version": 4,
21
21
  "sha256": "3357ea247694634c3535998368683eceffaf863221dee5fec9e43a03042afb95",
22
22
  "first_package": "0.3.36"
23
+ },
24
+ {
25
+ "check_version": 5,
26
+ "sha256": "8f36633fe693ebf7a7cc0f7237d49eb4720f67d371d4758d556bab51965ec334",
27
+ "first_package": "0.3.48"
23
28
  }
24
29
  ]
25
30
  }
@@ -101,7 +101,7 @@ set. A test keeps the two lists equal.
101
101
  | `atlas kind-drift` | When the repo record and `atlas.yaml` may disagree on the kind; this command retires in stage 2, the onboarding stage |
102
102
  | `atlas code-read` | To resolve the citations of a code digest |
103
103
  | `atlas ste` | Before each publish; `--share` before a page goes to anyone else |
104
- | `atlas tests` | `write` puts the test kit in an area. `check` runs before a merge. `kept` runs the kept tests of a path 1 area and writes their evidence. `proof`, `verdict`, `named`, `covers` and `rerun` read the results of a run. `changed` and `before` run test checks of the gate: the changed tests, and the proof that a new case fails on the old code. `sweep` and `issues` run the daily sweep |
104
+ | `atlas tests` | `write` puts the test kit in an area. `check` runs before a merge. `kept` runs the kept tests of a path 1 area and writes their evidence. `proof`, `verdict`, `named`, `covers` and `rerun` read the results of a run. `changed`, `before` and `after` run the test checks of the gate: changed tests, new cases fail on old code, old cases pass on new code. `sweep` and `issues` run the daily sweep |
105
105
  | `atlas design` | `folder` to learn where designs live; `check` before you show a design; `build` to make its page |
106
106
  | `atlas view` | `build --product <id>` makes the project view page of one product, when the owner asks for it or after an Atlas release changes it. Publish it with only the grant the command prints |
107
107
  | `atlas upkeep` | `check` before you ask for a merge: the docs index, dead doc links, and the tracker item a pull request names. The daily upkeep run calls it on the main branch |