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.
Files changed (43) hide show
  1. package/README.md +74 -16
  2. package/docs/prompt-integration.md +19 -5
  3. package/docs/stack-components.md +22 -5
  4. package/package.json +11 -1
  5. package/plugins/genesis/.codex-plugin/plugin.json +1 -1
  6. package/prompts/deslop.txt +3 -0
  7. package/prompts/master-pass.txt +17 -0
  8. package/prompts/pass-deep.txt +77 -0
  9. package/prompts/pass-report.txt +15 -0
  10. package/prompts/review.txt +2 -1
  11. package/prompts/security-pass.txt +26 -0
  12. package/prompts/speed-pass.txt +28 -0
  13. package/prompts/ui-pass.txt +3 -1
  14. package/prompts/ux-pass.txt +25 -0
  15. package/skills/genesis-data-pass/SKILL.md +29 -4
  16. package/skills/genesis-deslop/SKILL.md +47 -6
  17. package/skills/genesis-identity-pass/SKILL.md +29 -4
  18. package/skills/genesis-master-pass/SKILL.md +132 -0
  19. package/skills/genesis-master-pass/agents/openai.yaml +4 -0
  20. package/skills/genesis-quality-pass/SKILL.md +30 -4
  21. package/skills/genesis-review/SKILL.md +120 -0
  22. package/skills/genesis-review/agents/openai.yaml +4 -0
  23. package/skills/genesis-security-pass/SKILL.md +297 -0
  24. package/skills/genesis-security-pass/agents/openai.yaml +4 -0
  25. package/skills/genesis-speed-pass/SKILL.md +351 -0
  26. package/skills/genesis-speed-pass/agents/openai.yaml +4 -0
  27. package/skills/genesis-ui-pass/SKILL.md +38 -7
  28. package/skills/genesis-ux-pass/SKILL.md +276 -0
  29. package/skills/genesis-ux-pass/agents/openai.yaml +4 -0
  30. package/src/cli-commands.js +3 -1
  31. package/src/cli.js +14 -4
  32. package/src/index/agent-skills.js +53 -29
  33. package/src/index/assets.js +4 -1
  34. package/src/index/code-index.js +2 -1
  35. package/src/index/contracts.js +2 -0
  36. package/src/index/master-pass.js +76 -0
  37. package/src/index/pass-record.js +124 -22
  38. package/src/index/pass-support.js +71 -0
  39. package/src/index/passes.js +92 -5
  40. package/src/index/project-files.js +2 -1
  41. package/src/index/prompt.js +306 -28
  42. package/src/index/source-size.js +62 -0
  43. package/src/index.js +6 -1
@@ -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>" }`. The pass's own last step writes it, so it lands in the pass's commit and a pass
12
- // that never finished is never recorded.
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
- function invalidRecord(problem) {
19
- fail('PASS_RECORD_INVALID', `${PASS_RECORD_PATH} ${problem}. It maps pass ids (${PASS_IDS.join(', ')}) to UTC times such as 2026-01-31T09:30:00Z; fix or remove the entry.`, { path: PASS_RECORD_PATH });
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
- /** The project's pass record: each recorded pass id with the UTC time it last finished. A missing file records none. */
23
- export async function readPassRecord(root) {
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, PASS_RECORD_PATH), 'utf8');
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
- for (const [id, time] of Object.entries(record)) {
39
- if (!PASS_IDS.includes(id)) invalidRecord(`names ${JSON.stringify(id)}, which is not a pass`);
40
- if (typeof time !== 'string' || !UTC_TIME.test(time) || Number.isNaN(Date.parse(time))) {
41
- invalidRecord(`records ${JSON.stringify(time)} for ${id}, which is not a UTC time`);
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
- /** Records that `pass` finished now, as its last step. */
48
- export async function recordProjectPassDone({ projectRoot, pass, now = new Date() } = {}) {
49
- if (!PASS_IDS.includes(pass)) {
50
- fail('PASS_UNKNOWN', `${JSON.stringify(pass)} is not a pass; use one of ${PASS_IDS.join(', ')}.`);
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 record = { ...await readPassRecord(root), [pass]: lastRun };
59
- await writeFileAtomic(path.join(root, PASS_RECORD_PATH), stableJson(record));
60
- return { status: 'recorded', pass, lastRun, path: PASS_RECORD_PATH, changedFiles: [PASS_RECORD_PATH] };
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
+ }
@@ -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 request when none is given.
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 changes without changing what they do: no repeated helpers, unclear code or code that ignores the project\'s patterns.',
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 your own changes for the current task.',
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 your own changes for the current task.',
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
- async function visiblePaths(projectRoot) {
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', '--', '.']),