@arjunkhera/atlas 0.4.15 → 0.4.17
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 +2 -1
- package/door/lib/doc-sources.mjs +0 -1
- package/door/lib/kind-drift.mjs +1 -1
- package/door/lib/pull-request.mjs +18 -7
- package/door/lib/status.mjs +24 -9
- package/door/lib/tooling.mjs +28 -6
- package/door/lib/upkeep.mjs +1 -1
- package/package.json +1 -1
- package/shape/check.mjs +442 -305
- package/shape/releases.json +11 -0
package/door/cli.mjs
CHANGED
|
@@ -267,7 +267,8 @@ function toolingCommand(chosen) {
|
|
|
267
267
|
if (what !== 'write') throw new Error(`there is no tooling verb "${what}". Use write or pr.`);
|
|
268
268
|
const result = writeTooling(root, PACKAGE_ROOT, { dryRun: Boolean(chosen['dry-run']), replaceUnknown, workflowOnly });
|
|
269
269
|
line(`Atlas ${result.version}, check version ${result.check_version}${result.status ? `; the copy was ${result.status}` : ''}`);
|
|
270
|
-
|
|
270
|
+
// A removed file has no bytes, so its row has no sha256 (roadmap R7).
|
|
271
|
+
for (const row of result.rows) line(` ${row.state.padEnd(10)} ${row.path}${row.sha256 ? ` sha256 ${row.sha256.slice(0, 12)}…` : ''}`);
|
|
271
272
|
line('');
|
|
272
273
|
line(result.changed
|
|
273
274
|
? 'The files come from the package. No hand step, and no edit by hand afterwards.'
|
package/door/lib/doc-sources.mjs
CHANGED
|
@@ -96,7 +96,6 @@ export function docUnits(root) {
|
|
|
96
96
|
const units = [];
|
|
97
97
|
const folders = new Set();
|
|
98
98
|
for (const file of markdownFiles(root, 'docs')) {
|
|
99
|
-
if (file.startsWith('docs/site/')) continue;
|
|
100
99
|
const folder = designFolderOf(root, file);
|
|
101
100
|
if (folder) {
|
|
102
101
|
if (folders.has(folder)) continue;
|
package/door/lib/kind-drift.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Milestone M2, slice L2 stage one: "The facts helper compares the `kind` in
|
|
4
4
|
// `atlas.yaml` with the `kind` on the repo record and reports drift."
|
|
5
5
|
//
|
|
6
|
-
// The kind decides which of the
|
|
6
|
+
// The kind decides which of the six files a repository needs, so the two
|
|
7
7
|
// copies disagreeing is not a detail. A repo record that says `service` while
|
|
8
8
|
// atlas.yaml says `library` means four required parts read "not applicable" in
|
|
9
9
|
// CI and "required" everywhere else.
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
// git and GitHub calls sit below it, in one place.
|
|
12
12
|
import { execFileSync } from 'node:child_process';
|
|
13
13
|
import { childEnvironment } from '../../work/lib/child-env.mjs';
|
|
14
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
15
15
|
import { dirname, join, posix } from 'node:path';
|
|
16
|
-
import { CHECK_PATH, WORKFLOW_PATH } from '../../shape/check.mjs';
|
|
16
|
+
import { CHECK_PATH, WORKFLOW_PATH, OLD_WORKFLOW_PATH } from '../../shape/check.mjs';
|
|
17
17
|
|
|
18
18
|
// Every change names where it came from, and the set is closed.
|
|
19
19
|
export const PROVENANCE_SOURCES = Object.freeze(['owner words', 'repo read', 'task input']);
|
|
@@ -24,6 +24,9 @@ export const TOOLING_LABEL = 'atlas-helper';
|
|
|
24
24
|
// The only files the tooling writer may write: the check, its workflow, and
|
|
25
25
|
// the pull request template when the repo has none.
|
|
26
26
|
export const TOOLING_PATHS = Object.freeze([CHECK_PATH, WORKFLOW_PATH, '.github/workflows/atlas-jobs.yml', '.github/pull_request_template.md']);
|
|
27
|
+
// The only files the tooling writer may remove: the workflow of check versions
|
|
28
|
+
// 1 to 5, which the gate workflow replaces (roadmap R7).
|
|
29
|
+
export const TOOLING_REMOVALS = Object.freeze([OLD_WORKFLOW_PATH]);
|
|
27
30
|
|
|
28
31
|
export class ChangeError extends Error {}
|
|
29
32
|
|
|
@@ -38,8 +41,12 @@ export function checkChanges(changes) {
|
|
|
38
41
|
for (const change of changes) {
|
|
39
42
|
const path = posix.normalize(String(change?.path ?? '')).replace(/^\.\//, '');
|
|
40
43
|
if (!path || path === '.') throw new ChangeError('a change has no path');
|
|
41
|
-
if (
|
|
42
|
-
|
|
44
|
+
if (change.remove === true) {
|
|
45
|
+
if (!TOOLING_REMOVALS.includes(path)) throw new ChangeError(`the tooling writer removes only ${TOOLING_REMOVALS.join(' and ')}; it refuses to remove "${path}"`);
|
|
46
|
+
} else {
|
|
47
|
+
if (!TOOLING_PATHS.includes(path)) throw new ChangeError(`the tooling writer writes only ${TOOLING_PATHS.join(' and ')}; it refuses "${path}"`);
|
|
48
|
+
if (typeof change.contents !== 'string') throw new ChangeError(`the change to "${path}" carries no contents`);
|
|
49
|
+
}
|
|
43
50
|
const source = change?.provenance?.source;
|
|
44
51
|
if (!PROVENANCE_SOURCES.includes(source)) throw new ChangeError(`the change to "${path}" names no source; every change names one of ${PROVENANCE_SOURCES.join(', ')}`);
|
|
45
52
|
if (!String(change?.provenance?.detail ?? '').trim()) throw new ChangeError(`the change to "${path}" names the source "${source}" and says nothing about it`);
|
|
@@ -82,8 +89,11 @@ export function applyChanges(root, changes) {
|
|
|
82
89
|
const written = [];
|
|
83
90
|
for (const change of changes) {
|
|
84
91
|
const path = join(root, change.path);
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
if (change.remove === true) rmSync(path, { force: true });
|
|
93
|
+
else {
|
|
94
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
95
|
+
writeFileSync(path, change.contents);
|
|
96
|
+
}
|
|
87
97
|
written.push(change.path);
|
|
88
98
|
}
|
|
89
99
|
return { written };
|
|
@@ -105,7 +115,8 @@ export function open({ root, changes, why, branch, title, base = null, label = T
|
|
|
105
115
|
if (dryRun) return { repo, branch, title, body, base: onto, written, opened: false };
|
|
106
116
|
|
|
107
117
|
exec('git', ['checkout', '-b', branch], root);
|
|
108
|
-
|
|
118
|
+
// `git add -A` stages a removed file as well as a written one.
|
|
119
|
+
exec('git', ['add', '-A', '--', ...written], root);
|
|
109
120
|
exec('git', ['commit', '-m', title, '-m', body], root);
|
|
110
121
|
exec('git', ['push', '-u', 'origin', branch], root);
|
|
111
122
|
// A label that does not exist yet makes `gh pr create` fail, so it is made
|
package/door/lib/status.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// package's list. This runs in a session, with the package on disk.
|
|
16
16
|
import { readFileSync, existsSync, readdirSync, lstatSync } from 'node:fs';
|
|
17
17
|
import { join, relative, sep } from 'node:path';
|
|
18
|
-
import { CHECK_PATH, WORKFLOW_PATH, WORKFLOW_TEMPLATE, checkStamps, sha256 } from '../../shape/check.mjs';
|
|
18
|
+
import { CHECK_PATH, WORKFLOW_PATH, OLD_WORKFLOW_PATH, WORKFLOW_TEMPLATE, SHAPE_JOB_TEXT, shapeJobOf, checkStamps, sha256 } from '../../shape/check.mjs';
|
|
19
19
|
import { packagedCheck, placeOf, packagePackageVersion } from './releases.mjs';
|
|
20
20
|
import { currentKitHash, KIT_RELEASES_FILE, KIT_TARGET, MANIFEST } from './tests.mjs';
|
|
21
21
|
import { kitHashOf } from '../../tests/link-check.mjs';
|
|
@@ -57,8 +57,9 @@ export function statusMany(roots, packageRoot) {
|
|
|
57
57
|
// R19, slice 0 of the routines design). The check is one copy of four types:
|
|
58
58
|
//
|
|
59
59
|
// check .github/atlas/check.mjs, by the release list of the check
|
|
60
|
-
// workflow .github/workflows/atlas-
|
|
61
|
-
// in each row of that list
|
|
60
|
+
// workflow .github/workflows/atlas-gate.yml, by the workflow fingerprint
|
|
61
|
+
// in each row of that list, or by its shape job; before check
|
|
62
|
+
// version 6, .github/workflows/atlas-check.yml
|
|
62
63
|
// kit each <area>/test/atlas/, by its manifest and the kit list
|
|
63
64
|
// pin an Atlas version written in a workflow, a script or package.json
|
|
64
65
|
//
|
|
@@ -92,23 +93,37 @@ function checkRow(root, packageRoot) {
|
|
|
92
93
|
|
|
93
94
|
// The workflow is written with the check, from the WORKFLOW_TEMPLATE of that
|
|
94
95
|
// check. Each row of the release list keeps the fingerprint of its template.
|
|
96
|
+
// From check version 6 the workflow is the gate workflow, and a repo may add
|
|
97
|
+
// its own jobs to it, as the Atlas repo does: then its shape job must be the
|
|
98
|
+
// one the package writes. A repo that still has only the old atlas-check.yml
|
|
99
|
+
// is behind (roadmap R7 line 5).
|
|
95
100
|
function workflowRow(root, packageRoot) {
|
|
96
101
|
const pkg = packagedCheck(packageRoot);
|
|
97
102
|
const version = checkStamps(pkg.text).check_version;
|
|
98
103
|
const want = `workflow of check version ${version}`;
|
|
99
104
|
const full = join(root, WORKFLOW_PATH);
|
|
100
105
|
if (linkAt(full)) return linked('workflow', WORKFLOW_PATH, want);
|
|
101
|
-
|
|
106
|
+
const known = (text) => pkg.releases.filter((one) => one.workflow_sha256 === sha256(text));
|
|
107
|
+
if (!existsSync(full)) {
|
|
108
|
+
const old = join(root, OLD_WORKFLOW_PATH);
|
|
109
|
+
if (linkAt(old)) return linked('workflow', OLD_WORKFLOW_PATH, want);
|
|
110
|
+
if (!existsSync(old)) return row('workflow', WORKFLOW_PATH, 'missing', null, want, `${WORKFLOW_PATH} does not exist in this repo, so its check never runs in CI. "atlas tooling pr" adds it.`);
|
|
111
|
+
let text;
|
|
112
|
+
try { text = readFileSync(old, 'utf8'); } catch (error) { return broken('workflow', OLD_WORKFLOW_PATH, want, error); }
|
|
113
|
+
const rows = known(text);
|
|
114
|
+
if (rows.length) return row('workflow', OLD_WORKFLOW_PATH, 'behind', `workflow of check version ${rows.at(-1).check_version}`, want, `${OLD_WORKFLOW_PATH} runs the head's copy of the check, so a pull request can change its own judge. "atlas tooling pr" writes ${WORKFLOW_PATH} and removes it.`);
|
|
115
|
+
return row('workflow', OLD_WORKFLOW_PATH, 'unknown', `sha256 ${sha256(text).slice(0, 12)}…`, want, `${OLD_WORKFLOW_PATH} is in no release, and ${WORKFLOW_PATH} does not exist. "atlas tooling pr" shows the difference.`);
|
|
116
|
+
}
|
|
102
117
|
let text;
|
|
103
118
|
try { text = readFileSync(full, 'utf8'); } catch (error) { return broken('workflow', WORKFLOW_PATH, want, error); }
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
const rows =
|
|
119
|
+
if (text === WORKFLOW_TEMPLATE) return row('workflow', WORKFLOW_PATH, 'current', want, want, 'the workflow is the one this package writes.');
|
|
120
|
+
if (shapeJobOf(text) === SHAPE_JOB_TEXT) return row('workflow', WORKFLOW_PATH, 'current', want, want, 'its shape job is the one this package writes; the file holds other jobs of this repo too.');
|
|
121
|
+
const rows = known(text);
|
|
107
122
|
if (rows.length) {
|
|
108
123
|
const newest = rows.at(-1);
|
|
109
|
-
return row('workflow', WORKFLOW_PATH, 'behind', `workflow of check version ${newest.check_version}`, want, `the workflow was first written by Atlas ${rows[0].first_package}. "atlas tooling pr --workflow" refreshes it.`);
|
|
124
|
+
return row('workflow', WORKFLOW_PATH, 'behind', `workflow of check version ${newest.check_version}`, want, `the workflow was first written by Atlas ${rows[0].first_package ?? `check version ${rows[0].check_version}`}. "atlas tooling pr --workflow" refreshes it.`);
|
|
110
125
|
}
|
|
111
|
-
return row('workflow', WORKFLOW_PATH, 'unknown', `sha256 ${
|
|
126
|
+
return row('workflow', WORKFLOW_PATH, 'unknown', `sha256 ${sha256(text).slice(0, 12)}…`, want, `the workflow is in no release, and its shape job is not the one this package writes. Someone edited it, or it came from an unreleased package. "atlas tooling pr" shows the difference.`);
|
|
112
127
|
}
|
|
113
128
|
|
|
114
129
|
// A walk never follows a link and never enters node_modules or .git. The
|
package/door/lib/tooling.mjs
CHANGED
|
@@ -14,15 +14,27 @@
|
|
|
14
14
|
// is missing or behind, or unknown with --replace-unknown, and only a check
|
|
15
15
|
// that is the newest row of the release list. The Atlas repo itself may take
|
|
16
16
|
// its own unreleased check, because that is how a new check is made.
|
|
17
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
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, WORKFLOW_TEMPLATE, 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, 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';
|
|
24
24
|
|
|
25
|
-
export { CHECK_PATH, WORKFLOW_PATH, PACKAGE_CHECK_SOURCE, PR_TEMPLATE_PATH };
|
|
25
|
+
export { CHECK_PATH, WORKFLOW_PATH, OLD_WORKFLOW_PATH, PACKAGE_CHECK_SOURCE, PR_TEMPLATE_PATH };
|
|
26
|
+
|
|
27
|
+
// The gate workflow (roadmap R7 line 5). A repo may keep its own jobs in it, as
|
|
28
|
+
// the Atlas repo does. When its shape job is the one the package writes, the
|
|
29
|
+
// writer keeps the file. The workflow of check versions 1 to 5,
|
|
30
|
+
// atlas-check.yml, ran the head's copy of the check; the writer removes it.
|
|
31
|
+
const workflowKept = (root) => {
|
|
32
|
+
const full = join(root, WORKFLOW_PATH);
|
|
33
|
+
if (!existsSync(full)) return false;
|
|
34
|
+
const text = readFileSync(full, 'utf8');
|
|
35
|
+
return text !== WORKFLOW_TEMPLATE && shapeJobOf(text) === SHAPE_JOB_TEXT;
|
|
36
|
+
};
|
|
37
|
+
const oldWorkflow = (root) => existsSync(join(root, OLD_WORKFLOW_PATH));
|
|
26
38
|
|
|
27
39
|
// The watcher of the routines design (roadmap R24). The writer puts it into each
|
|
28
40
|
// repo, with the version of this package as its pin. It refreshes a copy whose pin
|
|
@@ -101,8 +113,8 @@ export function mayWrite(root, packageRoot, { replaceUnknown = false, workflowOn
|
|
|
101
113
|
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.`);
|
|
102
114
|
}
|
|
103
115
|
const now = status({ root, packageRoot });
|
|
104
|
-
if (now.state === 'current' && !workflowOnly && !templateMissing(root, packageRoot) && !jobsNeedWrite(root, packageRoot)) {
|
|
105
|
-
throw new ToolingRefusal(`this repo's check is already current. ${readWorkflow(root) === WORKFLOW_TEMPLATE ? '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 });
|
|
116
|
+
if (now.state === 'current' && !workflowOnly && !templateMissing(root, packageRoot) && !jobsNeedWrite(root, packageRoot) && !oldWorkflow(root)) {
|
|
117
|
+
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 });
|
|
106
118
|
}
|
|
107
119
|
if (now.state === 'unknown' && !replaceUnknown && !own) {
|
|
108
120
|
throw new ToolingRefusal(`${now.why}`);
|
|
@@ -120,6 +132,7 @@ export function write(root, packageRoot, { dryRun = false, replaceUnknown = fals
|
|
|
120
132
|
for (const [path, contents] of Object.entries(wanted)) {
|
|
121
133
|
const full = join(root, path);
|
|
122
134
|
const was = existsSync(full) ? readFileSync(full, 'utf8') : null;
|
|
135
|
+
if (path === WORKFLOW_PATH && workflowKept(root)) { rows.push({ path, state: 'kept', sha256: sha256(was), was: sha256(was) }); continue; }
|
|
123
136
|
const state = was === null ? 'written' : was === contents ? 'unchanged' : 'refreshed';
|
|
124
137
|
if (state !== 'unchanged' && !dryRun) {
|
|
125
138
|
mkdirSync(dirname(full), { recursive: true });
|
|
@@ -127,6 +140,11 @@ export function write(root, packageRoot, { dryRun = false, replaceUnknown = fals
|
|
|
127
140
|
}
|
|
128
141
|
rows.push({ path, state, sha256: sha256(contents), was: was === null ? null : sha256(was) });
|
|
129
142
|
}
|
|
143
|
+
if (oldWorkflow(root)) {
|
|
144
|
+
const was = readFileSync(join(root, OLD_WORKFLOW_PATH), 'utf8');
|
|
145
|
+
if (!dryRun) rmSync(join(root, OLD_WORKFLOW_PATH));
|
|
146
|
+
rows.push({ path: OLD_WORKFLOW_PATH, state: 'removed', sha256: null, was: sha256(was) });
|
|
147
|
+
}
|
|
130
148
|
const jobs = workflowOnly ? null : jobsState(root, packageRoot);
|
|
131
149
|
if (jobs !== null) {
|
|
132
150
|
const contents = packagedJobsWorkflow(packageRoot);
|
|
@@ -165,7 +183,11 @@ export function changes(root, packageRoot, { replaceUnknown = false, workflowOnl
|
|
|
165
183
|
const wanted = { ...packagedFiles(packageRoot), [JOBS_PATH]: packagedJobsWorkflow(packageRoot), [PR_TEMPLATE_PATH]: packagedTemplate(packageRoot) };
|
|
166
184
|
return rows
|
|
167
185
|
.filter((row) => row.state !== 'unchanged' && row.state !== 'kept')
|
|
168
|
-
.map((row) => ({
|
|
186
|
+
.map((row) => (row.state === 'removed' ? {
|
|
187
|
+
path: row.path,
|
|
188
|
+
remove: true,
|
|
189
|
+
provenance: { source: 'repo read', detail: `the workflow of check versions 1 to 5, which runs the head's copy of the check; ${WORKFLOW_PATH} replaces it from check version ${CHECK_VERSION}` },
|
|
190
|
+
} : {
|
|
169
191
|
path: row.path,
|
|
170
192
|
contents: wanted[row.path],
|
|
171
193
|
provenance: {
|
package/door/lib/upkeep.mjs
CHANGED
|
@@ -119,7 +119,7 @@ export function docsIndex(root) {
|
|
|
119
119
|
if (path.startsWith('docs/')) listed.add(path);
|
|
120
120
|
}
|
|
121
121
|
const unlisted = markdownFiles(root, 'docs').filter((path) => {
|
|
122
|
-
if (path === INDEX ||
|
|
122
|
+
if (path === INDEX || listed.has(path)) return false;
|
|
123
123
|
const folder = designFolderOf(root, path);
|
|
124
124
|
return !(folder && listed.has(`${folder}/design.yaml`));
|
|
125
125
|
});
|
package/package.json
CHANGED