@arjunkhera/atlas 0.4.44 → 0.4.46
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/.claude-plugin/plugin.json +1 -1
- package/door/cli.mjs +9 -41
- package/door/lib/owner-only.mjs +1 -1
- package/door/lib/tooling.mjs +32 -1
- package/door/lib/upkeep.mjs +14 -82
- package/owner-only.yaml +17 -9
- package/package.json +1 -1
- package/shape/check.mjs +436 -485
- package/shape/releases.json +10 -0
- package/skills/lead/SKILL.md +7 -5
- package/door/lib/kind-drift.mjs +0 -86
package/door/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// The atlas command: install, upgrade, doctor, check, status, the tooling
|
|
3
|
-
// writer, the
|
|
3
|
+
// writer, the code-read resolver, the STE check, the
|
|
4
4
|
// design check and page builder, the tests verbs (the link check and the
|
|
5
5
|
// test kit writer), the upkeep check, and the judge of the fresh-agent test.
|
|
6
6
|
//
|
|
@@ -18,7 +18,6 @@ import { spawnSync } from 'node:child_process';
|
|
|
18
18
|
import { open } from './lib/pull-request.mjs';
|
|
19
19
|
import { write as writeTooling, changes as toolingChanges, ToolingRefusal, CHECK_PATH, WORKFLOW_PATH } from './lib/tooling.mjs';
|
|
20
20
|
import { statusMany, copies } from './lib/status.mjs';
|
|
21
|
-
import { drift, kindsFromRegistry, fileKind } from './lib/kind-drift.mjs';
|
|
22
21
|
import { scan, toText, verdict, CHECK_VERSION, SPECS } from '../shape/check.mjs';
|
|
23
22
|
import { packagePackageVersion } from './lib/releases.mjs';
|
|
24
23
|
import { install, upgrade, doctor } from './lib/install.mjs';
|
|
@@ -54,9 +53,8 @@ const HELP = `atlas — the door into a repo's Atlas files
|
|
|
54
53
|
atlas status --root <repo> is the repo's check current, behind, unknown or missing
|
|
55
54
|
--all every Atlas copy in the repo: the check, its workflow, each test kit, each pinned Atlas version
|
|
56
55
|
--json print the rows as JSON: type, path, state, have, want, why
|
|
57
|
-
atlas kind-drift --root <repo> --record-kind <kinds>
|
|
58
|
-
does atlas.yaml agree with the repo record
|
|
59
56
|
atlas check --root <repo> run the repo shape check here
|
|
57
|
+
--json print the report as JSON, with the counts of parts, procedures and jobs
|
|
60
58
|
|
|
61
59
|
atlas code-read resolve … resolve the citations of a code digest
|
|
62
60
|
|
|
@@ -210,8 +208,7 @@ A person merges every file the tooling writer writes.
|
|
|
210
208
|
export const FLAGS = Object.freeze({
|
|
211
209
|
tooling: { root: 'optional', branch: 'optional', 'replace-unknown': 'switch', workflow: 'switch', 'dry-run': 'switch' },
|
|
212
210
|
status: { root: 'optional', all: 'switch', json: 'switch' },
|
|
213
|
-
|
|
214
|
-
check: { root: 'optional', repo: 'value', 'fail-on': 'value' },
|
|
211
|
+
check: { root: 'optional', repo: 'value', 'fail-on': 'value', json: 'switch' },
|
|
215
212
|
ste: { share: 'switch', terms: 'value' },
|
|
216
213
|
design: { tracker: 'value', out: 'value', draft: 'switch', root: 'optional' },
|
|
217
214
|
view: { product: 'value', name: 'value', out: 'value' },
|
|
@@ -338,40 +335,6 @@ function copiesCommand(roots, { all, json }) {
|
|
|
338
335
|
return repos.some((repo) => repo.rows.some((one) => bad.includes(one.state))) ? 1 : 0;
|
|
339
336
|
}
|
|
340
337
|
|
|
341
|
-
function kindDriftCommand(chosen) {
|
|
342
|
-
const root = rootOf(chosen);
|
|
343
|
-
let recordKind = chosen['record-kind'] ?? null;
|
|
344
|
-
if (!recordKind && chosen.registry) {
|
|
345
|
-
const kinds = kindsFromRegistry(JSON.parse(readFileSync(resolve(chosen.registry), 'utf8')));
|
|
346
|
-
const repo = chosen.repo ?? null;
|
|
347
|
-
recordKind = repo ? kinds[repo] ?? null : null;
|
|
348
|
-
if (!repo) {
|
|
349
|
-
// One checkout, so only the registry row that names THIS repo can be
|
|
350
|
-
// compared. Comparing every row against one root invented drift that was
|
|
351
|
-
// not there (acceptance review of batch 16, finding S2-6).
|
|
352
|
-
const here = fileKind(root);
|
|
353
|
-
if (!here.repo) { line(`${root} has no atlas.yaml, or its atlas.yaml names no repo, so no registry row can be matched to it.`); return 1; }
|
|
354
|
-
recordKind = kinds[here.repo] ?? null;
|
|
355
|
-
const others = Object.keys(kinds).filter((id) => id !== here.repo);
|
|
356
|
-
const report = drift([{ root, recordKind, recordRepo: here.repo }]);
|
|
357
|
-
for (const row of report.rows) line(`${row.state.toUpperCase().padEnd(10)} ${row.repo} file=${row.file_kind ?? '-'} record=${row.record_kind ?? '-'} ${row.why}`);
|
|
358
|
-
if (!(here.repo in kinds)) line(`The registry holds no row for ${here.repo}. Register it with the repo_add verb.`);
|
|
359
|
-
if (others.length) line(`Not this checkout, so not read: ${others.join(', ')}. Point --root at each one.`);
|
|
360
|
-
return report.drifting ? 1 : 0;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
const report = drift([{ root, recordKind }]);
|
|
364
|
-
for (const row of report.rows) {
|
|
365
|
-
line(`${row.state.toUpperCase()} ${row.repo ?? row.root}`);
|
|
366
|
-
line(` atlas.yaml kind ${row.file_kind ?? '(none)'} spec ${row.spec ?? '(none)'}`);
|
|
367
|
-
line(` repo record kind ${row.record_kind ?? '(not given)'}`);
|
|
368
|
-
line(` ${row.why}`);
|
|
369
|
-
}
|
|
370
|
-
line('');
|
|
371
|
-
line('Which of the two is the source is open question O-9. This report picks no winner.');
|
|
372
|
-
return report.drifting ? 1 : 0;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
338
|
function steCommand(chosen) {
|
|
376
339
|
const paths = chosen._.slice(1);
|
|
377
340
|
if (!paths.length) throw new Error('give a file or a folder: atlas ste <path> [--share]');
|
|
@@ -484,6 +447,12 @@ function checkCommand(chosen) {
|
|
|
484
447
|
const root = rootOf(chosen);
|
|
485
448
|
const report = scan({ root, repoId: chosen.repo ?? null });
|
|
486
449
|
const decision = verdict(report, chosen['fail-on'] ?? 'error');
|
|
450
|
+
// The O7 count is one command (roadmap R62 line 6): the report, its counts
|
|
451
|
+
// of listed parts, procedures and jobs, and the verdict.
|
|
452
|
+
if (chosen.json) {
|
|
453
|
+
process.stdout.write(`${JSON.stringify({ ...report, fail_on: chosen['fail-on'] ?? 'error', green: decision.green, why: decision.why }, null, 2)}\n`);
|
|
454
|
+
return decision.green ? 0 : 1;
|
|
455
|
+
}
|
|
487
456
|
process.stdout.write(toText(report));
|
|
488
457
|
// A report, never a reason for red: the list is an owner-only key.
|
|
489
458
|
const list = mergeList(root);
|
|
@@ -509,7 +478,6 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
509
478
|
case 'doctor': return doctor({ packageRoot: PACKAGE_ROOT, options: chosen, line });
|
|
510
479
|
case 'tooling': return toolingCommand(chosen);
|
|
511
480
|
case 'status': return statusCommand(chosen);
|
|
512
|
-
case 'kind-drift': return kindDriftCommand(chosen);
|
|
513
481
|
case 'check': return checkCommand(chosen);
|
|
514
482
|
case 'ste': return steCommand(chosen);
|
|
515
483
|
case 'design': return designCommand(chosen);
|
package/door/lib/owner-only.mjs
CHANGED
|
@@ -215,7 +215,7 @@ export function atlasYamlCause(baseText, headText, rules) {
|
|
|
215
215
|
if (!changed.length) return 'atlas.yaml text changed outside its keys';
|
|
216
216
|
for (const key of changed) {
|
|
217
217
|
if (rules.fixed.includes(key)) return `atlas.yaml key ${key} changed`;
|
|
218
|
-
if (!rules.add_only.includes(key)) return `atlas.yaml key ${key} changed, and
|
|
218
|
+
if (!rules.add_only.includes(key)) return `atlas.yaml key ${key} changed, and the key list in owner-only.yaml names no rule for it`;
|
|
219
219
|
if (!onlyAdds(a[key], b[key])) return `atlas.yaml key ${key}: an entry was removed, moved or changed`;
|
|
220
220
|
}
|
|
221
221
|
return null;
|
package/door/lib/tooling.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'node:fs';
|
|
18
18
|
import { execFileSync } from 'node:child_process';
|
|
19
19
|
import { dirname, join } from 'node:path';
|
|
20
|
-
import { CHECK_PATH, WORKFLOW_PATH, OLD_WORKFLOW_PATH, WORKFLOW_TEMPLATE, SHAPE_JOB_TEXT, shapeJobOf, CHECK_VERSION, sha256, parseYaml } from '../../shape/check.mjs';
|
|
20
|
+
import { CHECK_PATH, WORKFLOW_PATH, OLD_WORKFLOW_PATH, WORKFLOW_TEMPLATE, SHAPE_JOB_TEXT, shapeJobOf, CHECK_VERSION, SPECS, SPEC_3, sha256, parseYaml } from '../../shape/check.mjs';
|
|
21
21
|
import { packagedCheck, packagePackageVersion, PACKAGE_CHECK_SOURCE } from './releases.mjs';
|
|
22
22
|
import { status } from './status.mjs';
|
|
23
23
|
import { PR_TEMPLATE_PATH } from './upkeep.mjs';
|
|
@@ -122,12 +122,43 @@ export function isPackageRepo(root, packageRoot) {
|
|
|
122
122
|
// refusal that says what to do instead.
|
|
123
123
|
const readWorkflow = (root) => { const full = join(root, WORKFLOW_PATH); return existsSync(full) ? readFileSync(full, 'utf8') : null; };
|
|
124
124
|
|
|
125
|
+
// The spec 3 lines for a repo whose atlas.yaml is older (roadmap R62). Check
|
|
126
|
+
// version 10 reads spec 3 only, so a copy written into a spec 2 repo would turn
|
|
127
|
+
// its own check red. The lines name the parts this repo already carries: the
|
|
128
|
+
// two entry maps, the index and the two tooling files, plus the rules and a
|
|
129
|
+
// linter config where the repo has them.
|
|
130
|
+
export function specThreeLines(root) {
|
|
131
|
+
const parts = ['claude-md', 'agents-md'];
|
|
132
|
+
if (existsSync(join(root, '.claude', 'rules'))) parts.push('rules');
|
|
133
|
+
parts.push('docs-index', 'check-file', 'gate-workflow');
|
|
134
|
+
const linters = SPEC_3.files.flatMap((file) => file.parts).find((part) => part.id === 'linters').candidates;
|
|
135
|
+
if (linters.some((path) => existsSync(join(root, path)))) parts.push('linters');
|
|
136
|
+
return [`parts: [${parts.join(', ')}]`];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The spec version atlas.yaml declares, or null when there is no file or it
|
|
140
|
+
// does not read. The check reports those cases itself.
|
|
141
|
+
function declaredSpec(root) {
|
|
142
|
+
const file = join(root, 'atlas.yaml');
|
|
143
|
+
if (!existsSync(file)) return null;
|
|
144
|
+
try { return parseYaml(readFileSync(file, 'utf8'))?.spec ?? null; } catch { return null; }
|
|
145
|
+
}
|
|
146
|
+
|
|
125
147
|
export function mayWrite(root, packageRoot, { replaceUnknown = false, workflowOnly = false } = {}) {
|
|
126
148
|
const pkg = packagedCheck(packageRoot);
|
|
127
149
|
const own = isPackageRepo(root, packageRoot);
|
|
128
150
|
if (!pkg.latest && !own) {
|
|
129
151
|
throw new ToolingRefusal(`this package's ${PACKAGE_CHECK_SOURCE} is ${pkg.row ? 'not the newest release' : 'in no release'} of the check, so it is not written into another repo. Use a released Atlas.`);
|
|
130
152
|
}
|
|
153
|
+
const spec = declaredSpec(root);
|
|
154
|
+
if (spec !== null && !workflowOnly && !SPECS[spec]) {
|
|
155
|
+
throw new ToolingRefusal([
|
|
156
|
+
`atlas.yaml is spec ${spec}. Check version ${CHECK_VERSION} reads spec ${Object.keys(SPECS).join(', ')} only.`,
|
|
157
|
+
'Set spec: 3, remove the kind: line, and add:',
|
|
158
|
+
...specThreeLines(root),
|
|
159
|
+
'Then run atlas tooling again. Nothing was written.',
|
|
160
|
+
].join('\n'));
|
|
161
|
+
}
|
|
131
162
|
const now = status({ root, packageRoot });
|
|
132
163
|
if (now.state === 'current' && !workflowOnly && !templateMissing(root, packageRoot) && !pinnedNeedWrite(root, packageRoot) && !oldWorkflow(root)) {
|
|
133
164
|
throw new ToolingRefusal(`this repo's check is already current. ${readWorkflow(root) === WORKFLOW_TEMPLATE || workflowKept(root) ? 'Both files match the package, so there is nothing to write.' : 'Its workflow file differs from the package; pass --workflow to rewrite only that file.'}`, { current: true });
|
package/door/lib/upkeep.mjs
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
//
|
|
8
8
|
// 1. Docs index. docs/index.md lists each .md file under docs/. A file in a
|
|
9
9
|
// design folder (a folder that holds design.yaml) counts as listed when the
|
|
10
|
-
// index lists that folder's design.yaml. Each listed path exists.
|
|
10
|
+
// index lists that folder's design.yaml. Each listed path exists. The rule
|
|
11
|
+
// is the shape check's own, docsIndexRule (roadmap R62 line 5).
|
|
11
12
|
// 2. Dead links. Each relative link in a tracked .md file names a file or a
|
|
12
13
|
// folder that exists. archive/ is frozen, and test fixtures are planted on
|
|
13
14
|
// purpose, so both are skipped; so is text inside code.
|
|
@@ -24,8 +25,9 @@
|
|
|
24
25
|
// and a stale page is listed.
|
|
25
26
|
//
|
|
26
27
|
// It reads the tree and the event file, and writes nothing.
|
|
27
|
-
import { readFileSync, existsSync
|
|
28
|
-
import { join, dirname,
|
|
28
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
29
|
+
import { join, dirname, resolve } from 'node:path';
|
|
30
|
+
import { DOCS_INDEX_PATH, docsIndexRule, markdownFiles, proseOf, linkTargets } from '../../shape/check.mjs';
|
|
29
31
|
import { listChanged } from './changed-tests.mjs';
|
|
30
32
|
import { docSourceFindings, replay } from './doc-sources.mjs';
|
|
31
33
|
import { proceduresCommand } from './procedure-steps.mjs';
|
|
@@ -37,62 +39,13 @@ import { changedDocFindings } from './changed-docs.mjs';
|
|
|
37
39
|
// touched doc with no update and no mark a note, not a finding.
|
|
38
40
|
export const TOUCHED_DOCS_GATE = true;
|
|
39
41
|
|
|
40
|
-
const INDEX =
|
|
41
|
-
const SKIP_DIRS = new Set(['node_modules', 'archive', 'fixtures']);
|
|
42
|
+
const INDEX = DOCS_INDEX_PATH;
|
|
42
43
|
const MAX_LISTED = 20;
|
|
43
44
|
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const walk = (rel) => {
|
|
49
|
-
let entries;
|
|
50
|
-
try { entries = readdirSync(join(root, rel), { withFileTypes: true }); } catch { return; }
|
|
51
|
-
for (const entry of entries) {
|
|
52
|
-
if (entry.name.startsWith('.')) continue;
|
|
53
|
-
const path = rel ? `${rel}/${entry.name}` : entry.name;
|
|
54
|
-
if (entry.isDirectory()) { if (!SKIP_DIRS.has(entry.name)) walk(path); }
|
|
55
|
-
else if (entry.isFile() && entry.name.endsWith('.md')) out.push(path);
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
walk(under);
|
|
59
|
-
return out.sort();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Text outside code: fenced blocks of three or more marks, and inline code.
|
|
63
|
-
export function proseOf(text) {
|
|
64
|
-
const lines = String(text ?? '').split('\n');
|
|
65
|
-
const kept = [];
|
|
66
|
-
let fence = null;
|
|
67
|
-
for (const one of lines) {
|
|
68
|
-
const open = one.match(/^\s*(`{3,}|~{3,})/);
|
|
69
|
-
if (fence) {
|
|
70
|
-
if (open && open[1][0] === fence[0] && open[1].length >= fence.length && !one.trim().slice(open[1].length).trim()) fence = null;
|
|
71
|
-
kept.push('');
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
if (open) { fence = open[1]; kept.push(''); continue; }
|
|
75
|
-
kept.push(one.replace(/`+[^`\n]*`+/g, ''));
|
|
76
|
-
}
|
|
77
|
-
return kept.join('\n');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// The relative link targets in one markdown text, with their line numbers.
|
|
81
|
-
export function linkTargets(text) {
|
|
82
|
-
const out = [];
|
|
83
|
-
proseOf(text).split('\n').forEach((one, index) => {
|
|
84
|
-
for (const [, angled, bare] of one.matchAll(/\]\(\s*(?:<([^>\n]+)>|([^)\s]+))(?:\s+"[^"]*")?\s*\)/g)) {
|
|
85
|
-
const raw = angled ?? bare;
|
|
86
|
-
const target = raw.split('#')[0].split('?')[0];
|
|
87
|
-
if (!target || /^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.startsWith('#')) continue;
|
|
88
|
-
if (/[{}$*\\]/.test(target)) continue;
|
|
89
|
-
let decoded = target;
|
|
90
|
-
try { decoded = decodeURIComponent(target); } catch { /* keep it as written */ }
|
|
91
|
-
out.push({ target: decoded, line: index + 1 });
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
return out;
|
|
95
|
-
}
|
|
45
|
+
// markdownFiles, proseOf and linkTargets live in the shape check, beside the
|
|
46
|
+
// one docs index rule (roadmap R62 line 5). They stay exported here, so their
|
|
47
|
+
// readers keep one door.
|
|
48
|
+
export { markdownFiles, proseOf, linkTargets };
|
|
96
49
|
|
|
97
50
|
export function deadLinks(root) {
|
|
98
51
|
const dead = [];
|
|
@@ -106,32 +59,11 @@ export function deadLinks(root) {
|
|
|
106
59
|
return dead;
|
|
107
60
|
}
|
|
108
61
|
|
|
109
|
-
// The
|
|
110
|
-
|
|
111
|
-
let dir = posix.dirname(file);
|
|
112
|
-
while (dir && dir !== '.' && dir !== 'docs') {
|
|
113
|
-
if (existsSync(join(root, dir, 'design.yaml'))) return dir;
|
|
114
|
-
dir = posix.dirname(dir);
|
|
115
|
-
}
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
|
|
62
|
+
// The docs index rule is the shape check's, so the check and this command
|
|
63
|
+
// give one count on one tree (roadmap R62 line 5).
|
|
119
64
|
export function docsIndex(root) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const listed = new Set();
|
|
123
|
-
for (const { target } of linkTargets(readFileSync(join(root, INDEX), 'utf8'))) {
|
|
124
|
-
if (target.startsWith('/')) continue;
|
|
125
|
-
const path = posix.normalize(posix.join('docs', target)).replace(/\/$/, '');
|
|
126
|
-
if (path.startsWith('docs/')) listed.add(path);
|
|
127
|
-
}
|
|
128
|
-
const unlisted = markdownFiles(root, 'docs').filter((path) => {
|
|
129
|
-
if (path === INDEX || listed.has(path)) return false;
|
|
130
|
-
const folder = designFolderOf(root, path);
|
|
131
|
-
return !(folder && listed.has(`${folder}/design.yaml`));
|
|
132
|
-
});
|
|
133
|
-
const gone = [...listed].filter((path) => !existsSync(join(root, path))).sort();
|
|
134
|
-
return { checked: true, unlisted, gone };
|
|
65
|
+
const { checked, missing, unlisted, gone } = docsIndexRule(root);
|
|
66
|
+
return missing ? { checked, missing, unlisted, gone } : { checked, unlisted, gone };
|
|
135
67
|
}
|
|
136
68
|
|
|
137
69
|
const UUID = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i;
|
package/owner-only.yaml
CHANGED
|
@@ -218,22 +218,30 @@ floor:
|
|
|
218
218
|
# does not follow names out of it (shape/check.mjs, the exemption).
|
|
219
219
|
entry_map: docs/index.md
|
|
220
220
|
|
|
221
|
-
# The
|
|
222
|
-
#
|
|
223
|
-
#
|
|
221
|
+
# The key rule for atlas.yaml (roadmap R2 line 11, R62 line 3). Each key of
|
|
222
|
+
# atlas.yaml is on one of the two lists. A pull request may add entries under
|
|
223
|
+
# an add_only key and merge on green; any other change to it, and any change
|
|
224
|
+
# to a fixed key, comes to the owner. A key on neither list comes to the owner
|
|
225
|
+
# too, until a pull request puts it on one.
|
|
224
226
|
atlas_yaml:
|
|
225
227
|
add_only:
|
|
226
228
|
- parts
|
|
229
|
+
- procedures
|
|
230
|
+
- files
|
|
231
|
+
- kinds_of_change
|
|
227
232
|
fixed:
|
|
228
|
-
-
|
|
233
|
+
- spec
|
|
234
|
+
- product
|
|
235
|
+
- repo
|
|
229
236
|
- areas
|
|
237
|
+
- owner_only
|
|
230
238
|
- required_checks
|
|
231
|
-
- product
|
|
232
239
|
- product_paths
|
|
233
|
-
- repo
|
|
234
|
-
- kind
|
|
235
|
-
- kinds
|
|
236
240
|
- up_to_date
|
|
237
241
|
- brake
|
|
242
|
+
- live
|
|
243
|
+
- jobs
|
|
244
|
+
- design
|
|
238
245
|
- generated
|
|
239
|
-
-
|
|
246
|
+
- verbatim
|
|
247
|
+
- private_terms
|
package/package.json
CHANGED