@plexus-ms/lib 0.2.0 → 0.3.1

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 (44) hide show
  1. package/dist/bookkeeping.d.ts +26 -0
  2. package/dist/bookkeeping.d.ts.map +1 -0
  3. package/dist/bookkeeping.js +51 -0
  4. package/dist/bookkeeping.js.map +1 -0
  5. package/dist/consumers.d.ts +31 -0
  6. package/dist/consumers.d.ts.map +1 -0
  7. package/dist/consumers.js +70 -0
  8. package/dist/consumers.js.map +1 -0
  9. package/dist/context.d.ts +7 -1
  10. package/dist/context.d.ts.map +1 -1
  11. package/dist/context.js +19 -3
  12. package/dist/context.js.map +1 -1
  13. package/dist/frontmatter.d.ts +9 -0
  14. package/dist/frontmatter.d.ts.map +1 -0
  15. package/dist/frontmatter.js +42 -0
  16. package/dist/frontmatter.js.map +1 -0
  17. package/dist/index.d.ts +7 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +7 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/io.d.ts +6 -1
  22. package/dist/io.d.ts.map +1 -1
  23. package/dist/io.js +5 -1
  24. package/dist/io.js.map +1 -1
  25. package/dist/plan.d.ts +2 -0
  26. package/dist/plan.d.ts.map +1 -1
  27. package/dist/plan.js.map +1 -1
  28. package/dist/propagate.d.ts +20 -0
  29. package/dist/propagate.d.ts.map +1 -0
  30. package/dist/propagate.js +226 -0
  31. package/dist/propagate.js.map +1 -0
  32. package/dist/requirements.d.ts +27 -0
  33. package/dist/requirements.d.ts.map +1 -0
  34. package/dist/requirements.js +188 -0
  35. package/dist/requirements.js.map +1 -0
  36. package/dist/ship.d.ts +18 -0
  37. package/dist/ship.d.ts.map +1 -0
  38. package/dist/ship.js +131 -0
  39. package/dist/ship.js.map +1 -0
  40. package/dist/train.d.ts +45 -0
  41. package/dist/train.d.ts.map +1 -0
  42. package/dist/train.js +102 -0
  43. package/dist/train.js.map +1 -0
  44. package/package.json +2 -2
@@ -0,0 +1,226 @@
1
+ // propagate: bring every consumer of the presets up to a train (§ 3 PLX-Updates).
2
+ //
3
+ // `copier update` at every consumer site in the workspace (tenant repo roots
4
+ // and their apps/*), then the marker synced to the answers file, lockfiles
5
+ // refreshed where manifests moved, and the consumers' hand-owned workflows
6
+ // pinned to the train. A consumer is touched only on its expected branch and
7
+ // with no tracked modifications (consumers.ts). Propagation never aborts the
8
+ // run: a failing or conflicting site is recorded and the rest continues; the
9
+ // summary says what needs a human. Reviewing diffs and committing the
10
+ // consumers stays human.
11
+ //
12
+ // The dry run is an honest preview: each consumer is cloned into a scratch
13
+ // directory and copier-updated there from the LOCAL preset checkout at main,
14
+ // so the diff and any conflicts show before a tag exists. That scratch work
15
+ // goes through `Context.stage`: performed in every mode, since nothing the
16
+ // operator owns changes.
17
+ //
18
+ // plx propagate ~/dev # to the current train
19
+ // plx -n propagate ~/dev # preview in scratch clones
20
+ // plx -y propagate ~/dev --train 0.66.0 # unattended: copier takes defaults
21
+ import { join, relative } from 'node:path';
22
+ import { bumpWorkflowRefs, hasConflictMarkers, hasUnmergedEntries, syncMarker, WORKFLOW_REF } from "./bookkeeping.js";
23
+ import { findConsumers, presetSource } from "./consumers.js";
24
+ import { ANSWERS_FILE, parseAnswers } from "./copier.js";
25
+ import { MARKER_FILE } from "./marker.js";
26
+ import { overview } from "./overview.js";
27
+ import { run, write } from "./plan.js";
28
+ import { TRAIN_REPOS, UPSTREAM } from "./train.js";
29
+ /** True when nothing in the summary needs a human. */
30
+ export const isClean = (s) => s.conflicted.length === 0 && s.failed.length === 0 && s.skipped.length === 0;
31
+ const PREVIEW_BASE = 'ship-base';
32
+ export async function propagate(ctx, workspace, train) {
33
+ const summary = { train, preview: ctx.dryRun, clean: [], conflicted: [], failed: [], skipped: [] };
34
+ const upstream = join(workspace, UPSTREAM);
35
+ const tree = await overview(ctx, workspace);
36
+ const upstreamDir = tree.children.find((c) => c.path === upstream);
37
+ const presets = TRAIN_REPOS.filter((r) => upstreamDir?.children.some((c) => c.name === r && c.template));
38
+ const consumers = await findConsumers(ctx, tree, presets);
39
+ for (const c of consumers) {
40
+ if (!c.skipped)
41
+ continue;
42
+ ctx.say(`⚠ ${c.rel}: ${c.skipped} — skipped (fix it, then re-run propagate)`);
43
+ summary.skipped.push({ path: c.rel, reason: c.skipped });
44
+ }
45
+ const active = consumers.filter((c) => !c.skipped);
46
+ const scratch = ctx.dryRun ? (await ctx.stage(run('mktemp', ['-d'])))?.trim() : undefined;
47
+ const clones = new Map();
48
+ /** The preview clone of a consumer, made on first use, with the base marked for diffing manifests. */
49
+ const cloneOf = async (consumer) => {
50
+ let clone = clones.get(consumer.path);
51
+ if (!clone) {
52
+ clone = join(scratch ?? '', consumer.rel);
53
+ await ctx.stage(run('git', ['clone', '--quiet', consumer.path, clone]));
54
+ await ctx.stage(run('git', ['tag', PREVIEW_BASE], { cwd: clone }));
55
+ clones.set(consumer.path, clone);
56
+ }
57
+ return clone;
58
+ };
59
+ // Preview commits are plain plumbing on a throwaway clone: unsigned, no hooks, a stand-in identity.
60
+ const previewCommit = (clone, message) => ctx
61
+ .stage(run('git', ['add', '-A'], { cwd: clone }))
62
+ .then(() => ctx.stage(run('git', [
63
+ '-c',
64
+ 'commit.gpgsign=false',
65
+ '-c',
66
+ 'user.name=plx',
67
+ '-c',
68
+ 'user.email=plx@localhost',
69
+ 'commit',
70
+ '--quiet',
71
+ '--no-verify',
72
+ '--allow-empty',
73
+ '-m',
74
+ message,
75
+ ], { cwd: clone })));
76
+ try {
77
+ for (const preset of presets) {
78
+ const sites = active.flatMap((c) => c.sites.filter((s) => s.preset === preset).map((s) => [c, s]));
79
+ if (sites.length === 0)
80
+ ctx.say(`• no consumers of ${presetSource(preset)} found under ${workspace}`);
81
+ for (const [consumer, site] of sites)
82
+ await updateSite(ctx, consumer, site, join(upstream, preset), summary, cloneOf, previewCommit);
83
+ }
84
+ await refreshLockfiles(ctx, active, summary, clones);
85
+ await bumpConsumerRefs(ctx, consumers, train);
86
+ }
87
+ finally {
88
+ if (scratch)
89
+ await ctx.stage(run('rm', ['-rf', scratch]));
90
+ }
91
+ return summary;
92
+ }
93
+ async function updateSite(ctx, consumer, site, localPreset, summary, cloneOf, previewCommit) {
94
+ // The repo the update lands in and the site inside it: the consumer and the
95
+ // site themselves, or their counterparts in the preview clone.
96
+ let repo = consumer.path;
97
+ let target = site.path;
98
+ try {
99
+ if (ctx.dryRun) {
100
+ ctx.say(`→ ${site.rel}: copier update (preview from ${localPreset}@main)`);
101
+ repo = await cloneOf(consumer);
102
+ target = join(repo, relative(consumer.path, site.path));
103
+ // Point the clone's answers at the local preset so the preview reads it;
104
+ // copier refuses a dirty subproject, hence the commit. Template tasks
105
+ // (mise install, hook setup) are the real run's business.
106
+ const answers = await ctx.readFile(join(site.path, ANSWERS_FILE));
107
+ await ctx.stage(write(join(target, ANSWERS_FILE), answers.replace(/^_src_path: .*$/m, `_src_path: ${localPreset}`)));
108
+ await previewCommit(repo, `plx preview: ${site.rel} reads ${localPreset} from the local checkout`);
109
+ await ctx.stage(run('copier', ['update', '--skip-answered', '--trust', '--defaults', '--skip-tasks', '--vcs-ref', 'main'], {
110
+ cwd: target,
111
+ }));
112
+ }
113
+ else {
114
+ ctx.say(`→ ${site.rel}: copier update`);
115
+ // Unattended, a new question takes its default; on a terminal copier asks.
116
+ const args = ['update', '--skip-answered', '--trust', ...(ctx.yes ? ['--defaults'] : [])];
117
+ await ctx.apply([run('copier', args, { cwd: target, interactive: true })]);
118
+ }
119
+ }
120
+ catch (error) {
121
+ ctx.say(` ✗ FAILED — copier update exited nonzero${detail(error)}`);
122
+ summary.failed.push(site.rel);
123
+ return;
124
+ }
125
+ // copier applies the template diff with `git apply --3way`, which leaves
126
+ // unmerged index entries and inline markers when a hunk does not apply.
127
+ const status = await ctx.read('git', ['status', '--porcelain'], { cwd: repo });
128
+ let conflict = hasUnmergedEntries(status);
129
+ const changed = (await ctx.read('git', ['diff', '--name-only'], { cwd: repo })).split('\n').filter(Boolean);
130
+ for (const f of changed) {
131
+ const text = await ctx.readFile(join(repo, f)).catch(() => '');
132
+ if (hasConflictMarkers(text))
133
+ conflict = true;
134
+ }
135
+ await syncSiteMarker(ctx, site, target);
136
+ if (conflict) {
137
+ ctx.say(' ✗ CONFLICTS — resolve the <<<<<<< markers, then git add:');
138
+ summary.conflicted.push(site.rel);
139
+ }
140
+ else {
141
+ ctx.say(' ✓ merged clean:');
142
+ summary.clean.push(site.rel);
143
+ }
144
+ for (const line of status.split('\n').filter(Boolean))
145
+ ctx.say(` ${line}`);
146
+ // Each preview site is committed in the clone so the next site's status lists only its own changes.
147
+ if (ctx.dryRun)
148
+ await previewCommit(repo, `plx preview: ${site.rel} updated`);
149
+ }
150
+ /** The marker's `plexus` follows the answers file's `_commit` after an update (bookkeeping.ts). */
151
+ async function syncSiteMarker(ctx, site, target) {
152
+ const marker = await ctx.readFile(join(target, MARKER_FILE)).catch(() => undefined);
153
+ if (marker === undefined)
154
+ return;
155
+ const { commit } = parseAnswers(await ctx.readFile(join(target, ANSWERS_FILE)));
156
+ if (!commit)
157
+ return;
158
+ const synced = syncMarker(marker, commit);
159
+ if (synced === marker)
160
+ return;
161
+ ctx.say(` marker: plexus → ${commit} in ${site.rel}/${MARKER_FILE}`);
162
+ const step = write(join(target, MARKER_FILE), synced);
163
+ if (ctx.dryRun)
164
+ await ctx.stage(step);
165
+ else
166
+ await ctx.apply([step]);
167
+ }
168
+ /** Refresh the lockfile once per consumer whose manifests the updates touched. */
169
+ async function refreshLockfiles(ctx, consumers, summary, clones) {
170
+ const manifests = ['*package.json', 'pnpm-workspace.yaml'];
171
+ for (const c of consumers) {
172
+ if ((await ctx.readFile(join(c.path, 'pnpm-workspace.yaml')).catch(() => undefined)) === undefined)
173
+ continue;
174
+ const repo = ctx.dryRun ? clones.get(c.path) : c.path;
175
+ if (!repo)
176
+ continue;
177
+ const range = ctx.dryRun ? [PREVIEW_BASE] : [];
178
+ const unchanged = await ctx
179
+ .read('git', ['diff', '--quiet', ...range, '--', ...manifests], { cwd: repo })
180
+ .then(() => true)
181
+ .catch(() => false);
182
+ if (unchanged)
183
+ continue;
184
+ ctx.say(`→ ${c.rel}: pnpm install (manifests changed)`);
185
+ try {
186
+ await ctx.apply([run('mise', ['exec', '--', 'pnpm', 'install', '--reporter=silent'], { cwd: c.path })]);
187
+ }
188
+ catch (error) {
189
+ ctx.say(` ✗ pnpm install failed${detail(error)}`);
190
+ summary.failed.push(`${c.rel}/pnpm-lock.yaml`);
191
+ }
192
+ }
193
+ }
194
+ /** Consumers' hand-owned workflows pin ci-cd by tag; lockstep them to the train. */
195
+ async function bumpConsumerRefs(ctx, consumers, train) {
196
+ for (const c of consumers) {
197
+ const dir = join(c.path, '.github', 'workflows');
198
+ const entries = await ctx.readDir(dir).catch(() => []);
199
+ const files = entries
200
+ .filter((e) => !e.directory && /\.ya?ml$/.test(e.name))
201
+ .map((e) => e.name)
202
+ .sort();
203
+ if (files.length === 0)
204
+ continue;
205
+ if (c.skipped) {
206
+ ctx.say(`⚠ ${c.rel}: workflow refs not bumped (skipped above)`);
207
+ continue;
208
+ }
209
+ for (const f of files) {
210
+ const text = await ctx.readFile(join(dir, f));
211
+ if (!text.includes(WORKFLOW_REF))
212
+ continue;
213
+ const bumped = bumpWorkflowRefs(text, train);
214
+ if (bumped === text)
215
+ continue;
216
+ ctx.say(`→ ${c.rel}: ${f} ci-cd refs → @${train}`);
217
+ await ctx.apply([write(join(dir, f), bumped)]);
218
+ }
219
+ }
220
+ }
221
+ const detail = (error) => {
222
+ const message = error instanceof Error ? error.message : String(error);
223
+ const last = message.trim().split('\n').at(-1) ?? '';
224
+ return last && !/exited with/.test(last) ? `: ${last}` : '';
225
+ };
226
+ //# sourceMappingURL=propagate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"propagate.js","sourceRoot":"","sources":["../src/propagate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type { Context } from './context.ts';
2
+ export type PageMode = 'map' | 'module';
3
+ export interface Scan {
4
+ /** `✗ <name>: line N: ...`, in page order. */
5
+ defects: string[];
6
+ /** The requirement lines of a standard module; empty for anything else. */
7
+ requirements: string[];
8
+ /** The frontmatter fields the cross-page rules need. */
9
+ code?: string;
10
+ maturity?: string;
11
+ binds?: string;
12
+ depends: string[];
13
+ consumes: string[];
14
+ }
15
+ export declare function scanPage(text: string, name: string, mode: PageMode): Scan;
16
+ /** The requirement lines of a page, defects ignored: what `changed` compares across trains. */
17
+ export declare const extractRequirements: (text: string) => string[];
18
+ export declare const MAP_PAGE = "essentials/standard.md";
19
+ export interface CompendiumCheck {
20
+ defects: string[];
21
+ contracts: number;
22
+ approaches: number;
23
+ }
24
+ /** Every module page of a compendium checkout: `<dir>/<file>`, sorted, index pages left out. */
25
+ export declare function modulePages(ctx: Context, compendium: string): Promise<string[]>;
26
+ export declare function checkCompendium(ctx: Context, compendium: string): Promise<CompendiumCheck>;
27
+ //# sourceMappingURL=requirements.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requirements.d.ts","sourceRoot":"","sources":["../src/requirements.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,QAAQ,CAAC;AAExC,MAAM,WAAW,IAAI;IACnB,8CAA8C;IAC9C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,2EAA2E;IAC3E,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAgBD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CAwEzE;AAED,+FAA+F;AAC/F,eAAO,MAAM,mBAAmB,GAAI,MAAM,MAAM,KAAG,MAAM,EAA+C,CAAC;AAEzG,eAAO,MAAM,QAAQ,2BAA2B,CAAC;AAEjD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,gGAAgG;AAChG,wBAAsB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAQrF;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAuDhG"}
@@ -0,0 +1,188 @@
1
+ // The requirements check (§ 1.8 PLX), the compendium's `requirements.sh`
2
+ // moved: a verb, not a system.
3
+ //
4
+ // scanPage one page: its defects, and for a standard module its
5
+ // requirement lines, `§ <section> <text>`, in page order
6
+ // checkCompendium the map and every module page, plus the cross-page rules:
7
+ // unique codes, known dependencies, dependencies that respect
8
+ // the seams (§ 1.3 PLX)
9
+ //
10
+ // The map carries no requirements and no BCP 14 keyword. A module at standard
11
+ // maturity carries its requirements as blockquoted bullets ("> - ") under
12
+ // numbered section headings; a keyword anywhere else, a blockquote that is not
13
+ // such a bullet, or any keyword or blockquote in a module below standard
14
+ // maturity is a defect. The trailing "## Maintaining" section is prose
15
+ // throughout. Extraction is structural: no classification, no heuristics.
16
+ // `ship` compares a module's extracted lines across trains to stamp `changed`,
17
+ // so the extractor and the linter are one function and cannot disagree.
18
+ //
19
+ // plx requirements <compendium> # lint; exit 1 on any defect
20
+ // plx requirements --extract contracts/app.md
21
+ import { join } from 'node:path';
22
+ import { CATALOG_DIRS } from "./catalog.js";
23
+ const KEYWORD = /MUST|SHOULD|MAY/;
24
+ /** A BCP 14 keyword in a line, the informal plurals (MUSTs, SHOULDs, MAYs) not counting. */
25
+ const hasKeyword = (line) => KEYWORD.test(line.replace(/MUSTs|SHOULDs|MAYs/g, ''));
26
+ /** A bracketed, comma-separated list as the frontmatter writes it: `[A, B]`. */
27
+ const list = (value) => value
28
+ .replace(/^\s*\[/, '')
29
+ .replace(/\].*$/, '')
30
+ .split(',')
31
+ .map((s) => s.trim())
32
+ .filter(Boolean);
33
+ export function scanPage(text, name, mode) {
34
+ const scan = { defects: [], requirements: [], depends: [], consumes: [] };
35
+ const lines = text.split('\n');
36
+ const defect = (n, msg) => scan.defects.push(`✗ ${name}: line ${n + 1}: ${msg}`);
37
+ let i = 0;
38
+ if (lines[0] === '---') {
39
+ for (i = 1; i < lines.length && lines[i] !== '---'; i++) {
40
+ const line = lines[i] ?? '';
41
+ const [key = '', ...rest] = line.split(/\s+/);
42
+ const value = rest.join(' ');
43
+ if (key === 'maturity:')
44
+ scan.maturity = rest[0];
45
+ else if (key === 'code:')
46
+ scan.code = rest[0];
47
+ else if (key === 'binds:')
48
+ scan.binds = rest[0];
49
+ else if (key === 'depends:')
50
+ scan.depends = list(value);
51
+ else if (key === 'consumes:')
52
+ scan.consumes = list(value);
53
+ else if (key === 'kind:')
54
+ defect(i, 'kind is the folder, contracts/ or approaches/, not a field');
55
+ }
56
+ i++;
57
+ }
58
+ let inCode = false;
59
+ let maintaining = false;
60
+ let started = false;
61
+ let section = '';
62
+ for (; i < lines.length; i++) {
63
+ const line = lines[i] ?? '';
64
+ if (line.startsWith('```')) {
65
+ inCode = !inCode;
66
+ continue;
67
+ }
68
+ if (inCode)
69
+ continue;
70
+ if (line === '## Maintaining') {
71
+ maintaining = true;
72
+ section = '';
73
+ continue;
74
+ }
75
+ const heading = /^#+ § (\S+)/.exec(line);
76
+ if (heading) {
77
+ if (maintaining)
78
+ defect(i, 'numbered section after Maintaining');
79
+ section = heading[1] ?? '';
80
+ started = true;
81
+ continue;
82
+ }
83
+ if (line.startsWith('> - ')) {
84
+ if (mode === 'map')
85
+ defect(i, `requirement in the map: ${line}`);
86
+ else if (scan.maturity !== 'standard')
87
+ defect(i, `requirement in a module at ${scan.maturity} maturity: ${line}`);
88
+ else if (maintaining)
89
+ defect(i, `requirement under Maintaining: ${line}`);
90
+ else if (!started)
91
+ defect(i, 'requirement before the first § heading');
92
+ else
93
+ scan.requirements.push(`§ ${section} ${line.slice(4)}`);
94
+ continue;
95
+ }
96
+ if (line.startsWith('>')) {
97
+ defect(i, 'blockquote line is not a "> - " requirement bullet');
98
+ continue;
99
+ }
100
+ if (hasKeyword(line))
101
+ defect(i, `BCP 14 keyword outside a requirement: ${line}`);
102
+ }
103
+ if (mode === 'module') {
104
+ const { maturity = '', binds = '', code = '' } = scan;
105
+ // Reported at the page's last line, like awk's NR at END.
106
+ const n = text.endsWith('\n') ? lines.length - 1 : lines.length;
107
+ if (!/^(note|candidate|standard)$/.test(maturity))
108
+ defect(n - 1, `maturity must be note, candidate or standard, not "${maturity}"`);
109
+ if (!/^(repo|app|host|organisation)$/.test(binds))
110
+ defect(n - 1, `binds must be repo, app, host or organisation, not "${binds}"`);
111
+ if (maturity === 'note' && code !== '')
112
+ defect(n - 1, 'a note carries no code');
113
+ if (maturity !== 'note' && !/^PLX-[A-Z][A-Za-z]+$/.test(code))
114
+ defect(n - 1, `code must be PLX- plus one capitalised word, not "${code}"`);
115
+ }
116
+ return scan;
117
+ }
118
+ /** The requirement lines of a page, defects ignored: what `changed` compares across trains. */
119
+ export const extractRequirements = (text) => scanPage(text, '', 'module').requirements;
120
+ export const MAP_PAGE = 'essentials/standard.md';
121
+ /** Every module page of a compendium checkout: `<dir>/<file>`, sorted, index pages left out. */
122
+ export async function modulePages(ctx, compendium) {
123
+ const pages = [];
124
+ for (const dir of Object.keys(CATALOG_DIRS)) {
125
+ const entries = await ctx.readDir(join(compendium, dir));
126
+ for (const e of entries.filter((e) => !e.directory && e.name.endsWith('.md') && e.name !== 'index.md').sort())
127
+ pages.push(`${dir}/${e.name}`);
128
+ }
129
+ return pages;
130
+ }
131
+ export async function checkCompendium(ctx, compendium) {
132
+ const defects = [];
133
+ defects.push(...scanPage(await ctx.readFile(join(compendium, MAP_PAGE)), MAP_PAGE, 'map').defects);
134
+ const pages = await modulePages(ctx, compendium);
135
+ const scans = new Map();
136
+ for (const page of pages) {
137
+ const scan = scanPage(await ctx.readFile(join(compendium, page)), page, 'module');
138
+ defects.push(...scan.defects);
139
+ scans.set(page, scan);
140
+ }
141
+ // Module codes are unique across pages.
142
+ const byCode = new Map();
143
+ for (const [page, s] of scans)
144
+ if (s.code)
145
+ byCode.set(s.code, [...(byCode.get(s.code) ?? []), page]);
146
+ for (const [code, where] of byCode)
147
+ if (where.length > 1)
148
+ defects.push(`✗ duplicate module code: ${code}`);
149
+ // Every declared dependency names a module that exists.
150
+ for (const s of scans.values())
151
+ for (const dep of s.depends)
152
+ if (!byCode.has(dep))
153
+ defects.push(`✗ unknown module in depends: ${dep}`);
154
+ // Dependencies respect the seams (§ 1.3 PLX): a contract depends only on
155
+ // contracts, a dependency never crosses from the dev side (repo, app) to the
156
+ // ops side (host, organisation) or back, and what crosses a seam is listed
157
+ // under consumes and is a contract.
158
+ const kindOf = (page) => (page.startsWith('contracts/') ? 'contract' : 'approach');
159
+ const side = (binds) => (binds === 'repo' || binds === 'app' ? 'dev' : 'ops');
160
+ const kinds = new Map([...byCode].map(([code, [page = '']]) => [code, kindOf(page)]));
161
+ const binds = new Map([...byCode].map(([code, [page = '']]) => [code, scans.get(page)?.binds ?? '']));
162
+ for (const [page, s] of scans) {
163
+ const m = s.code ?? page.replace(/.*\//, '');
164
+ const kind = kindOf(page);
165
+ for (const c of s.consumes) {
166
+ if (kind === 'contract')
167
+ defects.push(`✗ ${m} is a contract and consumes ${c}; a contract has no other side to consume`);
168
+ else if (kinds.has(c) && kinds.get(c) !== 'contract')
169
+ defects.push(`✗ ${m} consumes ${c}, which is an approach; only a contract is consumed across a seam`);
170
+ else if (!kinds.has(c))
171
+ defects.push(`✗ ${m} consumes unknown module ${c}`);
172
+ }
173
+ for (const d of s.depends) {
174
+ if (!kinds.has(d))
175
+ continue;
176
+ if (kind === 'contract' && kinds.get(d) !== 'contract')
177
+ defects.push(`✗ ${m} is a contract and depends on the approach ${d}`);
178
+ if (side(s.binds ?? '') !== side(binds.get(d) ?? '') && kinds.get(d) !== 'contract')
179
+ defects.push(`✗ ${m} (${s.binds}) depends on ${d} (${binds.get(d)}) across the dev/ops seam; only a contract may be depended on across a seam`);
180
+ }
181
+ }
182
+ return {
183
+ defects,
184
+ contracts: pages.filter((p) => p.startsWith('contracts/')).length,
185
+ approaches: pages.filter((p) => p.startsWith('approaches/')).length,
186
+ };
187
+ }
188
+ //# sourceMappingURL=requirements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requirements.js","sourceRoot":"","sources":["../src/requirements.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,+BAA+B;AAC/B,EAAE;AACF,yEAAyE;AACzE,2EAA2E;AAC3E,8EAA8E;AAC9E,gFAAgF;AAChF,0CAA0C;AAC1C,EAAE;AACF,8EAA8E;AAC9E,0EAA0E;AAC1E,+EAA+E;AAC/E,yEAAyE;AACzE,uEAAuE;AACvE,0EAA0E;AAC1E,+EAA+E;AAC/E,wEAAwE;AACxE,EAAE;AACF,4EAA4E;AAC5E,gDAAgD;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAkB5C,MAAM,OAAO,GAAG,iBAAiB,CAAC;AAElC,4FAA4F;AAC5F,MAAM,UAAU,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,EAAE,CAAC,CAAC,CAAC;AAEpG,gFAAgF;AAChF,MAAM,IAAI,GAAG,CAAC,KAAa,EAAY,EAAE,CACvC,KAAK;KACF,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;KACrB,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;KACpB,KAAK,CAAC,GAAG,CAAC;KACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;KACpB,MAAM,CAAC,OAAO,CAAC,CAAC;AAErB,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,IAAY,EAAE,IAAc;IACjE,MAAM,IAAI,GAAS,EAAE,OAAO,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAChF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAG,CAAC,CAAS,EAAE,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,UAAU,CAAC,GAAG,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;IAEjG,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC;QACvB,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YACxD,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC5B,MAAM,CAAC,GAAG,GAAG,EAAE,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC7B,IAAI,GAAG,KAAK,WAAW;gBAAE,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;iBAC5C,IAAI,GAAG,KAAK,OAAO;gBAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;iBACzC,IAAI,GAAG,KAAK,QAAQ;gBAAE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;iBAC3C,IAAI,GAAG,KAAK,UAAU;gBAAE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;iBACnD,IAAI,GAAG,KAAK,WAAW;gBAAE,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;iBACrD,IAAI,GAAG,KAAK,OAAO;gBAAE,MAAM,CAAC,CAAC,EAAE,4DAA4D,CAAC,CAAC;QACpG,CAAC;QACD,CAAC,EAAE,CAAC;IACN,CAAC;IAED,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,WAAW,GAAG,KAAK,CAAC;IACxB,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,MAAM,GAAG,CAAC,MAAM,CAAC;YACjB,SAAS;QACX,CAAC;QACD,IAAI,MAAM;YAAE,SAAS;QACrB,IAAI,IAAI,KAAK,gBAAgB,EAAE,CAAC;YAC9B,WAAW,GAAG,IAAI,CAAC;YACnB,OAAO,GAAG,EAAE,CAAC;YACb,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,WAAW;gBAAE,MAAM,CAAC,CAAC,EAAE,oCAAoC,CAAC,CAAC;YACjE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC3B,OAAO,GAAG,IAAI,CAAC;YACf,SAAS;QACX,CAAC;QACD,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5B,IAAI,IAAI,KAAK,KAAK;gBAAE,MAAM,CAAC,CAAC,EAAE,2BAA2B,IAAI,EAAE,CAAC,CAAC;iBAC5D,IAAI,IAAI,CAAC,QAAQ,KAAK,UAAU;gBAAE,MAAM,CAAC,CAAC,EAAE,8BAA8B,IAAI,CAAC,QAAQ,cAAc,IAAI,EAAE,CAAC,CAAC;iBAC7G,IAAI,WAAW;gBAAE,MAAM,CAAC,CAAC,EAAE,kCAAkC,IAAI,EAAE,CAAC,CAAC;iBACrE,IAAI,CAAC,OAAO;gBAAE,MAAM,CAAC,CAAC,EAAE,wCAAwC,CAAC,CAAC;;gBAClE,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC7D,SAAS;QACX,CAAC;QACD,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,MAAM,CAAC,CAAC,EAAE,oDAAoD,CAAC,CAAC;YAChE,SAAS;QACX,CAAC;QACD,IAAI,UAAU,CAAC,IAAI,CAAC;YAAE,MAAM,CAAC,CAAC,EAAE,yCAAyC,IAAI,EAAE,CAAC,CAAC;IACnF,CAAC;IAED,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,MAAM,EAAE,QAAQ,GAAG,EAAE,EAAE,KAAK,GAAG,EAAE,EAAE,IAAI,GAAG,EAAE,EAAE,GAAG,IAAI,CAAC;QACtD,0DAA0D;QAC1D,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC;QAChE,IAAI,CAAC,6BAA6B,CAAC,IAAI,CAAC,QAAQ,CAAC;YAC/C,MAAM,CAAC,CAAC,GAAG,CAAC,EAAE,sDAAsD,QAAQ,GAAG,CAAC,CAAC;QACnF,IAAI,CAAC,gCAAgC,CAAC,IAAI,CAAC,KAAK,CAAC;YAC/C,MAAM,CAAC,CAAC,GAAG,CAAC,EAAE,uDAAuD,KAAK,GAAG,CAAC,CAAC;QACjF,IAAI,QAAQ,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE;YAAE,MAAM,CAAC,CAAC,GAAG,CAAC,EAAE,wBAAwB,CAAC,CAAC;QAChF,IAAI,QAAQ,KAAK,MAAM,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC;YAC3D,MAAM,CAAC,CAAC,GAAG,CAAC,EAAE,qDAAqD,IAAI,GAAG,CAAC,CAAC;IAChF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,+FAA+F;AAC/F,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,IAAY,EAAY,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC,YAAY,CAAC;AAEzG,MAAM,CAAC,MAAM,QAAQ,GAAG,wBAAwB,CAAC;AAQjD,gGAAgG;AAChG,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,GAAY,EAAE,UAAkB;IAChE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;QAC5C,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC;QACzD,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,IAAI,EAAE;YAC3G,KAAK,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,GAAY,EAAE,UAAkB;IACpE,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,OAAO,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC;IAEnG,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAgB,CAAC;IACtC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;QAClF,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9B,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACxB,CAAC;IAED,wCAAwC;IACxC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC3C,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,KAAK;QAAE,IAAI,CAAC,CAAC,IAAI;YAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;IACrG,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM;QAAE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,4BAA4B,IAAI,EAAE,CAAC,CAAC;IAE3G,wDAAwD;IACxD,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,EAAE;QAC5B,KAAK,MAAM,GAAG,IAAI,CAAC,CAAC,OAAO;YAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,OAAO,CAAC,IAAI,CAAC,gCAAgC,GAAG,EAAE,CAAC,CAAC;IAEzG,yEAAyE;IACzE,6EAA6E;IAC7E,2EAA2E;IAC3E,oCAAoC;IACpC,MAAM,MAAM,GAAG,CAAC,IAAY,EAA2B,EAAE,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IACpH,MAAM,IAAI,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACtF,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACtF,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACtG,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC;QAC9B,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1B,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC3B,IAAI,IAAI,KAAK,UAAU;gBACrB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,+BAA+B,CAAC,2CAA2C,CAAC,CAAC;iBAC7F,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,UAAU;gBAClD,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,mEAAmE,CAAC,CAAC;iBACnG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;gBAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,4BAA4B,CAAC,EAAE,CAAC,CAAC;QAC9E,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;YAC1B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;gBAAE,SAAS;YAC5B,IAAI,IAAI,KAAK,UAAU,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,UAAU;gBACpD,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,8CAA8C,CAAC,EAAE,CAAC,CAAC;YACxE,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,UAAU;gBACjF,OAAO,CAAC,IAAI,CACV,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,gBAAgB,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,6EAA6E,CAClI,CAAC;QACN,CAAC;IACH,CAAC;IAED,OAAO;QACL,OAAO;QACP,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM;QACjE,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM;KACpE,CAAC;AACJ,CAAC"}
package/dist/ship.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { Context } from './context.ts';
2
+ import { type Summary } from './propagate.ts';
3
+ export interface ShipOptions {
4
+ /** The workspace: the upstream umbrella and the consumers sit under it. */
5
+ workspace: string;
6
+ /** The train version; default: minor+1 over the latest, or the current one when only propagating. */
7
+ train?: string;
8
+ /** Skip the release; only propagate to the consumers. */
9
+ propagateOnly?: boolean;
10
+ /** The date stamped on the map, `YYYY-MM-DD`; default: today. */
11
+ today?: string;
12
+ }
13
+ export interface ShipResult extends Summary {
14
+ /** The version released, or already current when only propagating. */
15
+ released: boolean;
16
+ }
17
+ export declare function ship(ctx: Context, options: ShipOptions): Promise<ShipResult>;
18
+ //# sourceMappingURL=ship.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ship.d.ts","sourceRoot":"","sources":["../src/ship.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAG5C,OAAO,EAAa,KAAK,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAIzD,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,qGAAqG;IACrG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAW,SAAQ,OAAO;IACzC,sEAAsE;IACtE,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,wBAAsB,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CASlF"}
package/dist/ship.js ADDED
@@ -0,0 +1,131 @@
1
+ // ship: the release train (§ 1.5 PLX). Every train repo ships under ONE
2
+ // version, tagged identically whether or not it changed:
3
+ //
4
+ // preflight (every train repo on main, clean, not behind origin, the latest
5
+ // tags in agreement) → write the train bookkeeping (bookkeeping.ts: galaxy
6
+ // version, the preset's collection pin, workflow-ref examples, the map's
7
+ // version and timestamp, `changed` on every standard module whose extracted
8
+ // requirements moved) and commit it → tag every train repo → push →
9
+ // propagate to every consumer (propagate.ts).
10
+ //
11
+ // Substance commits stay human; ship writes only the mechanical bookkeeping.
12
+ // A failure between the first tag and the last push leaves a half-tagged
13
+ // train; the recovery is a re-run naming the same version, which skips the
14
+ // repos already tagged and pushes the rest.
15
+ //
16
+ // plx ship ~/dev # next train = minor+1 over the latest
17
+ // plx ship ~/dev --train 1.0.0 # explicit train version
18
+ // plx -n ship ~/dev # preview: list the bookkeeping, tags and
19
+ // # the propagation in scratch clones
20
+ // plx -y ship ~/dev # unattended: no prompt, copier takes defaults
21
+ import { join } from 'node:path';
22
+ import { bumpCollectionPin, bumpGalaxyVersion, bumpWorkflowRefs, stampChanged, stampStandard, today as todayOf, WORKFLOW_REF, } from "./bookkeeping.js";
23
+ import { frontmatterField } from "./frontmatter.js";
24
+ import { run, write } from "./plan.js";
25
+ import { propagate } from "./propagate.js";
26
+ import { extractRequirements, MAP_PAGE, modulePages } from "./requirements.js";
27
+ import { preflightRelease, readTrain, resolveVersion, UPSTREAM } from "./train.js";
28
+ export async function ship(ctx, options) {
29
+ const upstream = join(options.workspace, UPSTREAM);
30
+ const propagateOnly = options.propagateOnly ?? false;
31
+ const train = await readTrain(ctx, upstream);
32
+ if (!propagateOnly)
33
+ await preflightRelease(ctx, train);
34
+ const next = resolveVersion(train, options.train, propagateOnly);
35
+ if (!propagateOnly)
36
+ await release(ctx, train, next, options.today ?? todayOf());
37
+ const summary = await propagate(ctx, options.workspace, next);
38
+ return { ...summary, released: !propagateOnly };
39
+ }
40
+ async function release(ctx, train, next, today) {
41
+ ctx.say(`→ train ${train.last ?? 'none'} → ${next}`);
42
+ for (const repo of train.repos) {
43
+ if (repo.pending.length === 0)
44
+ ctx.say(` ${repo.name}: no changes since ${repo.latest} — tag only`);
45
+ else {
46
+ ctx.say(` ${repo.name} (since ${repo.latest ?? 'the beginning'}):`);
47
+ for (const line of repo.pending)
48
+ ctx.say(` ${line}`);
49
+ }
50
+ }
51
+ if (!(await ctx.confirm(`tag all ${train.repos.length} repos as ${next} and push?`)))
52
+ throw new Error('aborted');
53
+ try {
54
+ const plan = await planBookkeeping(ctx, train, next, today);
55
+ await ctx.apply(plan);
56
+ for (const repo of train.repos) {
57
+ const tagged = await ctx
58
+ .read('git', ['rev-parse', '-q', '--verify', `refs/tags/${next}`], { cwd: repo.path })
59
+ .then(() => true)
60
+ .catch(() => false);
61
+ if (tagged)
62
+ ctx.say(`• ${repo.name}: already tagged ${next}`);
63
+ else
64
+ await ctx.apply([run('git', ['tag', '-a', next, '-m', `train ${next}`], { cwd: repo.path })]);
65
+ await ctx.apply([run('git', ['push', 'origin', 'main', next], { cwd: repo.path })]);
66
+ }
67
+ }
68
+ catch (error) {
69
+ const message = error instanceof Error ? error.message : String(error);
70
+ throw new Error(`${message}\n✗ ship stopped during the release; re-run with --train ${next} to finish it (tagged repos are skipped)`);
71
+ }
72
+ }
73
+ /** The bookkeeping writes, then one commit per train repo that has any. */
74
+ async function planBookkeeping(ctx, train, next, today) {
75
+ const plan = [];
76
+ const touched = new Set();
77
+ const repo = (name) => {
78
+ const r = train.repos.find((r) => r.name === name);
79
+ if (!r)
80
+ throw new Error(`${name} is not a train repo`);
81
+ return r;
82
+ };
83
+ /** Plan a write when the transform changes the file, saying what it does; a missing file is left alone. */
84
+ const edit = async (r, rel, what, transform) => {
85
+ const path = join(r.path, rel);
86
+ const text = await ctx.readFile(path).catch(() => undefined);
87
+ if (text === undefined)
88
+ return;
89
+ const edited = transform(text);
90
+ if (edited === text)
91
+ return;
92
+ ctx.say(` ${r.name}: ${rel}: ${what}`);
93
+ plan.push(write(path, edited));
94
+ touched.add(r);
95
+ };
96
+ await edit(repo('platform'), 'ansible/galaxy.yml', `version → ${next}`, (t) => bumpGalaxyVersion(t, next));
97
+ // Only the plexus.platform collection's pin: a second collection in the same file stays untouched.
98
+ await edit(repo('preset-repo-platform'), 'template/requirements.yml', `plexus.platform pin → ${next}`, (t) => bumpCollectionPin(t, next));
99
+ for (const r of train.repos) {
100
+ const files = await ctx
101
+ .read('git', ['grep', '-l', WORKFLOW_REF, '--', '*.yml', '*.yaml', '*.jinja'], { cwd: r.path })
102
+ .catch(() => '');
103
+ for (const f of files.split('\n').filter(Boolean))
104
+ await edit(r, f, `ci-cd refs → @${next}`, (t) => bumpWorkflowRefs(t, next));
105
+ }
106
+ // The map carries the train version and the date it was cut (§ 1.5 PLX); a
107
+ // standard module is stamped `changed` when its extracted requirement lines
108
+ // differ from the previous train's, read with the same extractor the
109
+ // compendium's check runs, so the two never disagree.
110
+ const comp = repo('compendium');
111
+ await edit(comp, MAP_PAGE, `version → ${next}, timestamp → ${today}`, (t) => stampStandard(t, next, today));
112
+ for (const page of await modulePages(ctx, comp.path)) {
113
+ const text = await ctx.readFile(join(comp.path, page));
114
+ if (frontmatterField(text, 'maturity') !== 'standard')
115
+ continue;
116
+ const previous = comp.tag
117
+ ? await ctx.read('git', ['show', `${comp.tag}:${page}`], { cwd: comp.path }).catch(() => '')
118
+ : '';
119
+ if (extractRequirements(text).join('\n') === extractRequirements(previous).join('\n'))
120
+ continue;
121
+ await edit(comp, page, `requirements changed since ${comp.latest ?? 'the beginning'} → changed: ${next}`, (t) => stampChanged(t, next));
122
+ }
123
+ for (const r of train.repos) {
124
+ if (!touched.has(r))
125
+ continue;
126
+ plan.push(run('git', ['add', '-A'], { cwd: r.path }));
127
+ plan.push(run('git', ['commit', '--quiet', '-m', `chore: train ${next} bookkeeping`], { cwd: r.path }));
128
+ }
129
+ return plan;
130
+ }
131
+ //# sourceMappingURL=ship.js.map