genesis-compiler 1.10.20 → 1.10.22
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/README.md +74 -16
- package/docs/prompt-integration.md +19 -5
- package/docs/stack-components.md +22 -5
- package/package.json +11 -1
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/prompts/deslop.txt +3 -0
- package/prompts/master-pass.txt +17 -0
- package/prompts/pass-deep.txt +77 -0
- package/prompts/pass-report.txt +15 -0
- package/prompts/review.txt +2 -1
- package/prompts/security-pass.txt +26 -0
- package/prompts/speed-pass.txt +28 -0
- package/prompts/ui-pass.txt +3 -1
- package/prompts/ux-pass.txt +25 -0
- package/skills/genesis-data-pass/SKILL.md +29 -4
- package/skills/genesis-deslop/SKILL.md +47 -6
- package/skills/genesis-identity-pass/SKILL.md +29 -4
- package/skills/genesis-master-pass/SKILL.md +132 -0
- package/skills/genesis-master-pass/agents/openai.yaml +4 -0
- package/skills/genesis-quality-pass/SKILL.md +30 -4
- package/skills/genesis-review/SKILL.md +120 -0
- package/skills/genesis-review/agents/openai.yaml +4 -0
- package/skills/genesis-security-pass/SKILL.md +297 -0
- package/skills/genesis-security-pass/agents/openai.yaml +4 -0
- package/skills/genesis-speed-pass/SKILL.md +351 -0
- package/skills/genesis-speed-pass/agents/openai.yaml +4 -0
- package/skills/genesis-ui-pass/SKILL.md +38 -7
- package/skills/genesis-ux-pass/SKILL.md +276 -0
- package/skills/genesis-ux-pass/agents/openai.yaml +4 -0
- package/src/cli-commands.js +3 -1
- package/src/cli.js +14 -4
- package/src/index/agent-skills.js +53 -29
- package/src/index/assets.js +4 -1
- package/src/index/code-index.js +2 -1
- package/src/index/contracts.js +2 -0
- package/src/index/master-pass.js +76 -0
- package/src/index/pass-record.js +124 -22
- package/src/index/pass-support.js +71 -0
- package/src/index/passes.js +92 -5
- package/src/index/project-files.js +2 -1
- package/src/index/prompt.js +306 -28
- package/src/index/source-size.js +62 -0
- package/src/index.js +6 -1
package/src/index/pass-record.js
CHANGED
|
@@ -3,27 +3,50 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import { fail } from './errors.js';
|
|
5
5
|
import { gitContext } from './git.js';
|
|
6
|
-
import { PASSES } from './passes.js';
|
|
6
|
+
import { MASTER_PASS, PASSES } from './passes.js';
|
|
7
|
+
import { runGitText } from './process.js';
|
|
7
8
|
import { inspectProjectFormatAtRoot } from './project-format.js';
|
|
8
9
|
import { stableJson, writeFileAtomic } from './utils.js';
|
|
9
10
|
|
|
10
11
|
// When each pass last finished, committed with the project so every clone and host sees the same record:
|
|
11
|
-
// `{ "<pass id>": "<UTC time>" }
|
|
12
|
-
//
|
|
12
|
+
// `{ "<pass id>": "<UTC time>" }`, and the Master pass's time with the passes it ran,
|
|
13
|
+
// `"master-pass": { "lastRun": "<UTC time>", "passes": ["<pass id>", ...] }`. The pass's own last step writes it, so
|
|
14
|
+
// it lands in the pass's commit and a pass that never finished is never recorded.
|
|
15
|
+
// Two files, so that a Genesis older than a pass never meets its record: genesis-compiler 1.10.21 and earlier read only
|
|
16
|
+
// `genesis/passes.json` and refuse an id they do not know, so that file keeps the passes they know, and every pass
|
|
17
|
+
// added since, the Master pass among them, is recorded in `genesis/passes-added.json`. This Genesis keeps an entry it
|
|
18
|
+
// does not know, which a newer Genesis wrote, and leaves it unused, so a pass added later never strands it.
|
|
13
19
|
export const PASS_RECORD_PATH = 'genesis/passes.json';
|
|
20
|
+
export const ADDED_PASS_RECORD_PATH = 'genesis/passes-added.json';
|
|
14
21
|
|
|
22
|
+
const FIRST_PASS_IDS = Object.freeze([
|
|
23
|
+
'deslop', 'ui-pass', 'identity-pass', 'quality-pass', 'data-pass', 'security-pass', 'ux-pass', 'speed-pass',
|
|
24
|
+
]);
|
|
15
25
|
const PASS_IDS = PASSES.map(({ task }) => task);
|
|
26
|
+
const RECORDED_IDS = [...PASS_IDS, MASTER_PASS.task];
|
|
16
27
|
const UTC_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/u;
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
/** The file that records `pass`. */
|
|
30
|
+
export function passRecordPath(pass) {
|
|
31
|
+
return FIRST_PASS_IDS.includes(pass) ? PASS_RECORD_PATH : ADDED_PASS_RECORD_PATH;
|
|
20
32
|
}
|
|
21
33
|
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
function invalidRecord(file, problem) {
|
|
35
|
+
fail('PASS_RECORD_INVALID', `${file} ${problem}. It maps pass ids to UTC times such as 2026-01-31T09:30:00Z, and ${MASTER_PASS.task} to { "lastRun": <UTC time>, "passes": [<pass id>, ...] }; fix or remove the entry.`, { path: file });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const utcTime = (value) => typeof value === 'string' && UTC_TIME.test(value) && !Number.isNaN(Date.parse(value));
|
|
39
|
+
|
|
40
|
+
// The passes a Master pass ran: pass ids, each once, at least one. A record may name a pass a newer Genesis added; a
|
|
41
|
+
// Master pass recorded here names only passes this Genesis has.
|
|
42
|
+
const passList = (value, known = null) => Array.isArray(value) && value.length > 0
|
|
43
|
+
&& new Set(value).size === value.length
|
|
44
|
+
&& value.every((id) => typeof id === 'string' && (!known || known.includes(id)));
|
|
45
|
+
|
|
46
|
+
async function readRecordFile(root, file) {
|
|
24
47
|
let source;
|
|
25
48
|
try {
|
|
26
|
-
source = await readFile(path.join(root,
|
|
49
|
+
source = await readFile(path.join(root, file), 'utf8');
|
|
27
50
|
} catch (error) {
|
|
28
51
|
if (error?.code === 'ENOENT') return {};
|
|
29
52
|
throw error;
|
|
@@ -32,30 +55,109 @@ export async function readPassRecord(root) {
|
|
|
32
55
|
try {
|
|
33
56
|
record = JSON.parse(source);
|
|
34
57
|
} catch (error) {
|
|
35
|
-
invalidRecord(`is not JSON (${error.message})`);
|
|
58
|
+
invalidRecord(file, `is not JSON (${error.message})`);
|
|
36
59
|
}
|
|
37
|
-
if (record === null || typeof record !== 'object' || Array.isArray(record)) invalidRecord('is not a JSON object');
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
60
|
+
if (record === null || typeof record !== 'object' || Array.isArray(record)) invalidRecord(file, 'is not a JSON object');
|
|
61
|
+
return record;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The project's pass record, from both files: each recorded pass id with the UTC time it last finished, and the Master
|
|
66
|
+
* pass's time with the passes it ran; and, as `unknown`, the entries this Genesis does not know, which it keeps and
|
|
67
|
+
* does not use. Missing files record none.
|
|
68
|
+
* @returns {Promise<{ record: object, unknown: { id: string, path: string }[] }>}
|
|
69
|
+
*/
|
|
70
|
+
export async function inspectPassRecord(root) {
|
|
71
|
+
const record = {};
|
|
72
|
+
const unknown = [];
|
|
73
|
+
for (const file of [PASS_RECORD_PATH, ADDED_PASS_RECORD_PATH]) {
|
|
74
|
+
for (const [id, value] of Object.entries(await readRecordFile(root, file))) {
|
|
75
|
+
if (!RECORDED_IDS.includes(id)) {
|
|
76
|
+
unknown.push({ id, path: file });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (passRecordPath(id) !== file) invalidRecord(file, `records ${id}, which belongs in ${passRecordPath(id)}; move the entry there`);
|
|
80
|
+
if (id === MASTER_PASS.task) {
|
|
81
|
+
if (!utcTime(value?.lastRun) || !passList(value?.passes) || Object.keys(value).length !== 2) {
|
|
82
|
+
invalidRecord(file, `records ${JSON.stringify(value)} for ${id}, which is not a UTC time with the passes it ran`);
|
|
83
|
+
}
|
|
84
|
+
} else if (!utcTime(value)) {
|
|
85
|
+
invalidRecord(file, `records ${JSON.stringify(value)} for ${id}, which is not a UTC time`);
|
|
86
|
+
}
|
|
87
|
+
record[id] = value;
|
|
42
88
|
}
|
|
43
89
|
}
|
|
44
|
-
return record;
|
|
90
|
+
return { record, unknown };
|
|
45
91
|
}
|
|
46
92
|
|
|
47
|
-
/**
|
|
48
|
-
export async function
|
|
49
|
-
|
|
50
|
-
|
|
93
|
+
/** The project's pass record: each pass id this Genesis knows with its last finished run. */
|
|
94
|
+
export async function readPassRecord(root) {
|
|
95
|
+
return (await inspectPassRecord(root)).record;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** What a reader says about entries it does not know: a newer Genesis wrote them, and updating Genesis uses them. */
|
|
99
|
+
export function unknownRecordMessage(unknown) {
|
|
100
|
+
const names = unknown.map(({ id, path: file }) => `${id} (${file})`).join(', ');
|
|
101
|
+
return `The pass record names passes this Genesis does not know: ${names}. A newer Genesis recorded them; they are `
|
|
102
|
+
+ 'kept as they are. Update genesis-compiler to the release that has them to use those records.';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Records that `pass` finished now, as its last step; the Master pass records the passes it ran with it, in the order
|
|
107
|
+
* it runs them. Entries this Genesis does not know stay as they are.
|
|
108
|
+
*/
|
|
109
|
+
export async function recordProjectPassDone({ projectRoot, pass, passes = [], now = new Date() } = {}) {
|
|
110
|
+
if (!RECORDED_IDS.includes(pass)) {
|
|
111
|
+
fail('PASS_UNKNOWN', `${JSON.stringify(pass)} is not a pass this Genesis knows; use one of ${RECORDED_IDS.join(', ')}, or update genesis-compiler to the release that added it.`);
|
|
112
|
+
}
|
|
113
|
+
const master = pass === MASTER_PASS.task;
|
|
114
|
+
if (master ? !passList(passes, PASS_IDS) : passes.length > 0) {
|
|
115
|
+
fail('PASS_RECORD_PASSES_INVALID', master
|
|
116
|
+
? `The ${MASTER_PASS.title} records the passes it ran, each once: name at least one of ${PASS_IDS.join(', ')}.`
|
|
117
|
+
: `Only the ${MASTER_PASS.title} records the passes it ran; record ${pass} on its own.`);
|
|
51
118
|
}
|
|
52
119
|
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
53
120
|
const format = await inspectProjectFormatAtRoot(root);
|
|
54
121
|
if (format.status !== 'current') {
|
|
55
122
|
fail('PASS_RECORD_PROJECT_REQUIRED', `Passes are recorded in a current Genesis project; this one is ${format.status} (next action: ${format.action}).`);
|
|
56
123
|
}
|
|
124
|
+
await inspectPassRecord(root);
|
|
125
|
+
const file = passRecordPath(pass);
|
|
57
126
|
const lastRun = now.toISOString().replace(/\.\d{3}Z$/u, 'Z');
|
|
58
|
-
const
|
|
59
|
-
await
|
|
60
|
-
|
|
127
|
+
const ran = MASTER_PASS.order.filter((id) => passes.includes(id));
|
|
128
|
+
const entries = { ...await readRecordFile(root, file), [pass]: master ? { lastRun, passes: ran } : lastRun };
|
|
129
|
+
await writeFileAtomic(path.join(root, file), stableJson(entries));
|
|
130
|
+
return {
|
|
131
|
+
status: 'recorded',
|
|
132
|
+
pass,
|
|
133
|
+
lastRun,
|
|
134
|
+
...(master ? { passes: ran } : {}),
|
|
135
|
+
path: file,
|
|
136
|
+
changedFiles: [file],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The commit that recorded a pass's last completed run, which is the pass's own commit, and the commits since it on
|
|
142
|
+
* HEAD; null when the run is not committed or that commit is no longer in HEAD's history. Git keeps it: the commit
|
|
143
|
+
* that added this exact entry to the pass's record file (for the Master pass, its time), so nothing else is stored.
|
|
144
|
+
* @param {string} root the project's repository root
|
|
145
|
+
* @param {string} pass the pass id
|
|
146
|
+
* @param {string | null} lastRun the run's UTC time, from readPassRecord
|
|
147
|
+
* @returns {Promise<{ commit: string, commitsSince: number } | null>}
|
|
148
|
+
*/
|
|
149
|
+
export async function passRecordCommit(root, pass, lastRun) {
|
|
150
|
+
if (!lastRun) return null;
|
|
151
|
+
try {
|
|
152
|
+
await runGitText(root, ['rev-parse', '--verify', '--quiet', 'HEAD^{commit}']);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
// A repository without commits has no history to find the run in.
|
|
155
|
+
if (error?.code === 'GIT_EXEC_FAILED') return null;
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
const entry = pass === MASTER_PASS.task ? `"lastRun": "${lastRun}"` : `"${pass}": "${lastRun}"`;
|
|
159
|
+
const commit = await runGitText(root, ['log', '-1', '--format=%h', `-S${entry}`, 'HEAD', '--', passRecordPath(pass)]);
|
|
160
|
+
if (!commit) return null;
|
|
161
|
+
const commitsSince = Number(await runGitText(root, ['rev-list', '--count', `${commit}..HEAD`]));
|
|
162
|
+
return { commit, commitsSince };
|
|
61
163
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { projectGenesis } from './agent-skills.js';
|
|
5
|
+
import { GENESIS_CONTRACTS } from './contracts.js';
|
|
6
|
+
|
|
7
|
+
// The npm option that keeps genesis-compiler in the manifest field the project declares it in.
|
|
8
|
+
const SAVE_OPTIONS = Object.freeze({
|
|
9
|
+
dependencies: '--save',
|
|
10
|
+
devDependencies: '--save-dev',
|
|
11
|
+
optionalDependencies: '--save-optional',
|
|
12
|
+
});
|
|
13
|
+
let hostManifest;
|
|
14
|
+
|
|
15
|
+
async function readManifest(directory) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(await readFile(path.join(directory, 'package.json'), 'utf8'));
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (['ENOENT', 'ENOTDIR'].includes(error?.code)) return null;
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Which of this Genesis's passes the project's own Genesis runs. A pass's prompt sends the agent to that Genesis's
|
|
26
|
+
* workflow skill, and its PASS RECORD step to that Genesis's `passes done`, so that Genesis alone decides, from what
|
|
27
|
+
* it ships: it runs passes when its package declares this pass-run contract (`genesisContracts.passRuns`), and it has
|
|
28
|
+
* a pass when its skills folder holds the pass's workflow skill, and it runs each pass's Deep run when its package also
|
|
29
|
+
* declares the deep-run contract (`genesisContracts.deepRuns`). A project pinned to a release without either, or
|
|
30
|
+
* whose pinned release is not installed, is told plainly, with the step that fixes it; nothing else stands in for
|
|
31
|
+
* that release.
|
|
32
|
+
* @param {string} projectRoot the project's repository root
|
|
33
|
+
*/
|
|
34
|
+
export async function projectPassSupport(projectRoot) {
|
|
35
|
+
hostManifest ??= projectGenesis().then(({ root }) => readManifest(root));
|
|
36
|
+
const [genesis, host, project] = await Promise.all([
|
|
37
|
+
projectGenesis({ projectRoot }).catch((error) => {
|
|
38
|
+
// A project that declares genesis-compiler without installing it has no Genesis to run a pass yet.
|
|
39
|
+
if (error?.code === 'AGENT_SKILL_UNAVAILABLE') return null;
|
|
40
|
+
throw error;
|
|
41
|
+
}),
|
|
42
|
+
hostManifest,
|
|
43
|
+
readManifest(projectRoot),
|
|
44
|
+
]);
|
|
45
|
+
const own = genesis && await readManifest(genesis.root);
|
|
46
|
+
const named = `This project's Genesis (genesis-compiler ${own?.version || 'of unknown version'})`;
|
|
47
|
+
const field = Object.keys(SAVE_OPTIONS).find((name) => Object.hasOwn(project?.[name] || {}, 'genesis-compiler'));
|
|
48
|
+
const update = field
|
|
49
|
+
? `Update it to ${host.version} or later with npm install ${SAVE_OPTIONS[field]} genesis-compiler@${host.version}, then commit the change.`
|
|
50
|
+
: `Update it to ${host.version} or later.`;
|
|
51
|
+
const declared = own?.genesisContracts?.passRuns;
|
|
52
|
+
let unavailable = null;
|
|
53
|
+
if (!genesis) {
|
|
54
|
+
unavailable = "This project's Genesis (genesis-compiler) is not installed. Install the project's dependencies with npm install.";
|
|
55
|
+
} else if (declared !== GENESIS_CONTRACTS.passRuns) {
|
|
56
|
+
unavailable = declared
|
|
57
|
+
? `${named} runs passes differently from genesis-compiler ${host.version}. Update the older of the two.`
|
|
58
|
+
: `${named} cannot run passes. ${update}`;
|
|
59
|
+
}
|
|
60
|
+
const has = (pass) => Boolean(genesis?.coreSkills.includes(`genesis-${pass.task}`));
|
|
61
|
+
return {
|
|
62
|
+
/** Why the project's Genesis runs no pass, naming the update; null when it runs them. */
|
|
63
|
+
unavailable,
|
|
64
|
+
/** Whether the project's Genesis has the pass's task, which its prompt needs even when no pass is recorded. */
|
|
65
|
+
has,
|
|
66
|
+
/** Whether the project's Genesis runs each pass's Deep run: its package declares this deep-run contract. */
|
|
67
|
+
deep: unavailable === null && own.genesisContracts.deepRuns === GENESIS_CONTRACTS.deepRuns,
|
|
68
|
+
/** Why the project's Genesis cannot run this pass, naming the update; null when it can. */
|
|
69
|
+
unavailableFor: (pass) => unavailable ?? (has(pass) ? null : `${named} has no ${pass.title}. ${update}`),
|
|
70
|
+
};
|
|
71
|
+
}
|
package/src/index/passes.js
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The passes: operations a person asks for by name over a scope of the project, never run because a turn finished.
|
|
3
3
|
* Each is a prompt task (`task`) with a workflow skill (`genesis-<task>`) and a Stack section (`section`), whose prose
|
|
4
|
-
* the selected components add to the task's prompt under `heading`. `request` is the
|
|
4
|
+
* the selected components add to the task's prompt under `heading`. `request` is the pass's own request, which names
|
|
5
|
+
* the whole app: a pass covers the whole app, and Genesis records a pass only when its prompt carries this request.
|
|
6
|
+
* The task's request when none is given is the task's own (prompt.js), which for Deslop and the UI pass stays the
|
|
7
|
+
* agent's own changes.
|
|
5
8
|
* A host lists the passes with inspectPasses() and decides itself when to offer one: `recommend` is a hint
|
|
6
|
-
* (`when`: `after-change`, `first-version` or `periodic`), never a schedule.
|
|
9
|
+
* (`when`: `after-change`, `first-version` or `periodic`), never a schedule. A host may also run a pass over the
|
|
10
|
+
* changes since a base commit it chooses, such as where a piece of work started (baseRequest); Genesis never records
|
|
11
|
+
* those runs, so they never move the pass's last completed run.
|
|
12
|
+
* `selfContained` marks a pass whose prompt carries all its instructions, without its skill: it was a task before it
|
|
13
|
+
* was a pass, so it still runs as that task, and over a base, where the project's own Genesis lacks the pass.
|
|
7
14
|
*/
|
|
8
15
|
export const PASSES = Object.freeze([
|
|
9
16
|
{
|
|
10
17
|
task: 'deslop',
|
|
11
18
|
title: 'Deslop',
|
|
12
|
-
description: 'Simplifies
|
|
19
|
+
description: 'Simplifies the app\'s code without changing what it does: no repeated helpers, unclear code or code that ignores the project\'s patterns.',
|
|
13
20
|
recommend: { when: 'after-change', hint: 'after each saved change' },
|
|
14
|
-
request: 'Deslop
|
|
21
|
+
request: 'Deslop the whole app.',
|
|
15
22
|
section: 'Deslop',
|
|
16
23
|
field: 'deslop',
|
|
17
24
|
heading: 'SELECTED STACK CLEANUP GUIDANCE',
|
|
@@ -21,7 +28,7 @@ export const PASSES = Object.freeze([
|
|
|
21
28
|
title: 'UI pass',
|
|
22
29
|
description: 'Makes screens follow the design rules of the project\'s technology exactly, without changing what they do.',
|
|
23
30
|
recommend: { when: 'after-change', hint: 'after a change to the screens' },
|
|
24
|
-
request: 'UI pass
|
|
31
|
+
request: 'UI pass the whole app.',
|
|
25
32
|
section: 'UI pass',
|
|
26
33
|
field: 'uiPass',
|
|
27
34
|
heading: 'SELECTED STACK UI PASS GUIDANCE',
|
|
@@ -56,4 +63,84 @@ export const PASSES = Object.freeze([
|
|
|
56
63
|
field: 'dataPass',
|
|
57
64
|
heading: 'SELECTED STACK DATA PASS GUIDANCE',
|
|
58
65
|
},
|
|
66
|
+
{
|
|
67
|
+
task: 'security-pass',
|
|
68
|
+
title: 'Security pass',
|
|
69
|
+
description: 'Makes the app refuse what it should: permission checked on every action, guarded sign-in, inputs checked at the boundary, no exposed secrets and safe production settings, each fix proved by trying the attack.',
|
|
70
|
+
recommend: { when: 'periodic', hint: 'after a stretch of changes, about every few weeks of active work, and before a first release' },
|
|
71
|
+
request: 'Security pass the whole app.',
|
|
72
|
+
section: 'Security pass',
|
|
73
|
+
field: 'securityPass',
|
|
74
|
+
heading: 'SELECTED STACK SECURITY PASS GUIDANCE',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
task: 'ux-pass',
|
|
78
|
+
title: 'User experience pass',
|
|
79
|
+
description: 'Makes the main things people do short and clear: fewer steps and decisions, easy to find, plain words, good defaults and mistakes easy to undo, each task walked before and after.',
|
|
80
|
+
recommend: { when: 'periodic', hint: 'after a stretch of changes, about every few weeks of active work, and before new people start using the app' },
|
|
81
|
+
request: 'User experience pass the whole app.',
|
|
82
|
+
section: 'User experience pass',
|
|
83
|
+
field: 'uxPass',
|
|
84
|
+
heading: 'SELECTED STACK UX PASS GUIDANCE',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
task: 'speed-pass',
|
|
88
|
+
title: 'Speed pass',
|
|
89
|
+
description: 'Makes the app and its tests fast: tests prepared once and run in parallel, screens that load only what they need, and requests that do each piece of work once, each wait measured before and after with nothing weakened.',
|
|
90
|
+
recommend: { when: 'periodic', hint: 'after a stretch of changes, about every few weeks of active work, and whenever tests, pages or saves feel slow' },
|
|
91
|
+
request: 'Speed pass the whole app.',
|
|
92
|
+
section: 'Speed pass',
|
|
93
|
+
field: 'speedPass',
|
|
94
|
+
heading: 'SELECTED STACK SPEED PASS GUIDANCE',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
task: 'review',
|
|
98
|
+
title: 'Review',
|
|
99
|
+
description: 'Checks the work against what the app should do: a test that would fail without each behavior, nothing weakened or skipped, and gaps between intent, code and explanation, changing only tests.',
|
|
100
|
+
recommend: { when: 'after-change', hint: 'after each saved change' },
|
|
101
|
+
request: 'Review the whole app.',
|
|
102
|
+
section: 'Review',
|
|
103
|
+
field: 'review',
|
|
104
|
+
heading: 'SELECTED STACK REVIEW GUIDANCE',
|
|
105
|
+
selfContained: true,
|
|
106
|
+
},
|
|
59
107
|
].map((pass) => Object.freeze({ ...pass, recommend: Object.freeze(pass.recommend) })));
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The Master pass: the passes run deeply, one after another, each as its own Deep run, committed and tested before the
|
|
111
|
+
* next starts. It runs every pass the project offers, or the ones a request names, always in `order`, whatever order
|
|
112
|
+
* they were named in: the review first, so every later pass starts from tested work, then security, so later passes
|
|
113
|
+
* build on the access it fixes, and Deslop last, so it cleans up after what the others changed. Every pass has a place
|
|
114
|
+
* in `order`; a pass added later takes one there.
|
|
115
|
+
*/
|
|
116
|
+
export const MASTER_PASS = Object.freeze({
|
|
117
|
+
task: 'master-pass',
|
|
118
|
+
title: 'Master pass',
|
|
119
|
+
description: 'Runs every pass deeply, one after another, or the passes you choose, committing and running the whole test suite after each.',
|
|
120
|
+
request: 'Master pass the whole app.',
|
|
121
|
+
order: Object.freeze([
|
|
122
|
+
'review', 'security-pass', 'data-pass', 'quality-pass', 'speed-pass', 'ux-pass', 'identity-pass', 'ui-pass', 'deslop',
|
|
123
|
+
]),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The request of a pass's Deep run: its pass request deeply, or, for a Master pass since its last completed run, the
|
|
128
|
+
* changes since that commit deeply, judged against the whole app.
|
|
129
|
+
* @param {{ title: string, request: string }} pass
|
|
130
|
+
* @param {string | null} [commit] the commit of the Master pass's last completed run, for a run since it
|
|
131
|
+
*/
|
|
132
|
+
export function deepRequest(pass, commit = null) {
|
|
133
|
+
return commit
|
|
134
|
+
? `${pass.title} deeply: the changes since commit ${commit}, judged against the whole app.`
|
|
135
|
+
: pass.request.replace(/\.$/u, ' deeply.');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The request of a run over the changes since a base commit a host chooses, judged against the whole app, or of its
|
|
140
|
+
* deep form; `{base}` stands for the commit. Genesis never records these runs.
|
|
141
|
+
* @param {{ title: string }} pass
|
|
142
|
+
* @param {boolean} [deep]
|
|
143
|
+
*/
|
|
144
|
+
export function baseRequest(pass, deep = false) {
|
|
145
|
+
return `${pass.title}${deep ? ' deeply' : ''}: the changes since base commit {base}, judged against the whole app.`;
|
|
146
|
+
}
|
|
@@ -4,7 +4,8 @@ import { runGit } from './process.js';
|
|
|
4
4
|
import { pathState } from './utils.js';
|
|
5
5
|
import { inspectProjectContent } from './project-inspection.js';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/** Git-visible paths: tracked and untracked files that are not ignored or deleted, sorted. */
|
|
8
|
+
export async function visiblePaths(projectRoot) {
|
|
8
9
|
const [visible, deleted] = await Promise.all([
|
|
9
10
|
runGit(projectRoot, ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '.']),
|
|
10
11
|
runGit(projectRoot, ['ls-files', '-z', '--deleted', '--', '.']),
|