@aperant/framework 0.6.5 → 0.6.7
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/CHANGELOG.md +155 -0
- package/README.md +17 -251
- package/dist/cli/commands/check-version.d.mts.map +1 -1
- package/dist/cli/commands/check-version.mjs +76 -1
- package/dist/cli/commands/check-version.mjs.map +1 -1
- package/dist/cli/commands/ci-watch.d.mts.map +1 -1
- package/dist/cli/commands/ci-watch.mjs +34 -4
- package/dist/cli/commands/ci-watch.mjs.map +1 -1
- package/dist/cli/commands/commit.d.mts.map +1 -1
- package/dist/cli/commands/commit.mjs +25 -8
- package/dist/cli/commands/commit.mjs.map +1 -1
- package/dist/cli/commands/event.d.mts.map +1 -1
- package/dist/cli/commands/event.mjs +66 -0
- package/dist/cli/commands/event.mjs.map +1 -1
- package/dist/cli/commands/init.d.mts +8 -1
- package/dist/cli/commands/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +91 -9
- package/dist/cli/commands/init.mjs.map +1 -1
- package/dist/cli/commands/modes.d.mts.map +1 -1
- package/dist/cli/commands/modes.mjs +2 -1
- package/dist/cli/commands/modes.mjs.map +1 -1
- package/dist/cli/commands/release-notes.d.mts +11 -0
- package/dist/cli/commands/release-notes.d.mts.map +1 -0
- package/dist/cli/commands/release-notes.mjs +173 -0
- package/dist/cli/commands/release-notes.mjs.map +1 -0
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +285 -139
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/commands/triage.d.mts.map +1 -1
- package/dist/cli/commands/triage.mjs +9 -5
- package/dist/cli/commands/triage.mjs.map +1 -1
- package/dist/cli/config/load.d.mts +14 -0
- package/dist/cli/config/load.d.mts.map +1 -1
- package/dist/cli/config/load.mjs +40 -0
- package/dist/cli/config/load.mjs.map +1 -1
- package/dist/cli/config/upgrade-gitignore.d.mts.map +1 -1
- package/dist/cli/config/upgrade-gitignore.mjs +6 -0
- package/dist/cli/config/upgrade-gitignore.mjs.map +1 -1
- package/dist/cli/consistency/parse-review.d.mts.map +1 -1
- package/dist/cli/consistency/parse-review.mjs +4 -1
- package/dist/cli/consistency/parse-review.mjs.map +1 -1
- package/dist/cli/coordination/auto-emit-artifact-ready.d.mts +16 -0
- package/dist/cli/coordination/auto-emit-artifact-ready.d.mts.map +1 -0
- package/dist/cli/coordination/auto-emit-artifact-ready.mjs +88 -0
- package/dist/cli/coordination/auto-emit-artifact-ready.mjs.map +1 -0
- package/dist/cli/coordination/event-schema.d.mts +16 -0
- package/dist/cli/coordination/event-schema.d.mts.map +1 -0
- package/dist/cli/coordination/event-schema.mjs +161 -0
- package/dist/cli/coordination/event-schema.mjs.map +1 -0
- package/dist/cli/coordination/store.d.mts +6 -0
- package/dist/cli/coordination/store.d.mts.map +1 -1
- package/dist/cli/coordination/store.mjs +14 -0
- package/dist/cli/coordination/store.mjs.map +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +2 -0
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
- package/dist/cli/gate/gates/review-clean.mjs +4 -2
- package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
- package/dist/cli/help.mjs +2 -2
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/install/update-chips.d.mts +23 -0
- package/dist/cli/install/update-chips.d.mts.map +1 -1
- package/dist/cli/install/update-chips.mjs +60 -0
- package/dist/cli/install/update-chips.mjs.map +1 -1
- package/dist/cli/release-notes/compile.d.mts +38 -0
- package/dist/cli/release-notes/compile.d.mts.map +1 -0
- package/dist/cli/release-notes/compile.mjs +244 -0
- package/dist/cli/release-notes/compile.mjs.map +1 -0
- package/dist/cli/release-notes/draft.d.mts +73 -0
- package/dist/cli/release-notes/draft.d.mts.map +1 -0
- package/dist/cli/release-notes/draft.mjs +120 -0
- package/dist/cli/release-notes/draft.mjs.map +1 -0
- package/dist/cli/release-notes/output-dir.d.mts +20 -0
- package/dist/cli/release-notes/output-dir.d.mts.map +1 -0
- package/dist/cli/release-notes/output-dir.mjs +42 -0
- package/dist/cli/release-notes/output-dir.mjs.map +1 -0
- package/dist/cli/release-notes/persona-filter.d.mts +51 -0
- package/dist/cli/release-notes/persona-filter.d.mts.map +1 -0
- package/dist/cli/release-notes/persona-filter.mjs +127 -0
- package/dist/cli/release-notes/persona-filter.mjs.map +1 -0
- package/dist/cli/release-notes/publish.d.mts +23 -0
- package/dist/cli/release-notes/publish.d.mts.map +1 -0
- package/dist/cli/release-notes/publish.mjs +125 -0
- package/dist/cli/release-notes/publish.mjs.map +1 -0
- package/dist/cli/release-notes/ship-autodraft.d.mts +37 -0
- package/dist/cli/release-notes/ship-autodraft.d.mts.map +1 -0
- package/dist/cli/release-notes/ship-autodraft.mjs +97 -0
- package/dist/cli/release-notes/ship-autodraft.mjs.map +1 -0
- package/dist/cli/roadmap/conductor-view.d.mts +13 -0
- package/dist/cli/roadmap/conductor-view.d.mts.map +1 -0
- package/dist/cli/roadmap/conductor-view.mjs +31 -0
- package/dist/cli/roadmap/conductor-view.mjs.map +1 -0
- package/dist/cli/route/skill-discover.d.mts.map +1 -1
- package/dist/cli/route/skill-discover.mjs +2 -1
- package/dist/cli/route/skill-discover.mjs.map +1 -1
- package/dist/cli/task/ids.d.mts +7 -4
- package/dist/cli/task/ids.d.mts.map +1 -1
- package/dist/cli/task/ids.mjs +11 -10
- package/dist/cli/task/ids.mjs.map +1 -1
- package/dist/types/config.d.ts +11 -7
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +9 -1
- package/prompts/conductor-status-check.md +23 -0
- package/prompts/conductor-sub-agent.md +57 -0
- package/prompts/conductor-system.md +172 -0
- package/skills/apt-close-task/SKILL.md +25 -0
- package/skills/apt-diagram/SKILL.md +45 -9
- package/skills/apt-plan/SKILL.md +12 -0
- package/skills/apt-plan/adapters/conductor.md +98 -0
- package/skills/apt-release-notes/SKILL.md +193 -0
- package/skills/apt-release-notes/appendices/persona-voice.md +59 -0
- package/skills/apt-setup/SKILL.md +148 -3
- package/skills/apt-ship/SKILL.md +74 -12
- package/skills/apt-spar/SKILL.md +290 -0
- package/skills/apt-update/SKILL.md +51 -9
- package/skills/apt-watch-ci/SKILL.md +2 -2
- package/src/cli/commands/check-version.mjs +73 -1
- package/src/cli/commands/ci-watch.mjs +35 -4
- package/src/cli/commands/commit.mjs +27 -8
- package/src/cli/commands/event.mjs +68 -0
- package/src/cli/commands/init.mjs +101 -9
- package/src/cli/commands/modes.mjs +2 -1
- package/src/cli/commands/release-notes.mjs +187 -0
- package/src/cli/commands/task.mjs +305 -152
- package/src/cli/commands/triage.mjs +14 -5
- package/src/cli/config/load.mjs +37 -0
- package/src/cli/config/upgrade-gitignore.mjs +6 -0
- package/src/cli/consistency/parse-review.mjs +3 -1
- package/src/cli/coordination/auto-emit-artifact-ready.mjs +96 -0
- package/src/cli/coordination/event-schema.d.ts +13 -0
- package/src/cli/coordination/event-schema.mjs +174 -0
- package/src/cli/coordination/store.mjs +14 -0
- package/src/cli/dispatch.mjs +2 -0
- package/src/cli/gate/gates/review-clean.mjs +4 -2
- package/src/cli/help.mjs +2 -2
- package/src/cli/install/update-chips.mjs +57 -0
- package/src/cli/release-notes/compile.mjs +261 -0
- package/src/cli/release-notes/draft.mjs +133 -0
- package/src/cli/release-notes/output-dir.mjs +52 -0
- package/src/cli/release-notes/persona-filter.mjs +126 -0
- package/src/cli/release-notes/publish.mjs +128 -0
- package/src/cli/release-notes/ship-autodraft.mjs +106 -0
- package/src/cli/roadmap/conductor-view.d.ts +10 -0
- package/src/cli/roadmap/conductor-view.mjs +31 -0
- package/src/cli/route/skill-discover.mjs +2 -1
- package/src/cli/task/ids.mjs +15 -13
- package/templates/config.json +28 -3
- package/workflows/docs.md +12 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* release-notes/compile.mjs — tag-time narrative compiler.
|
|
3
|
+
*
|
|
4
|
+
* Reads every `<output_dir>/_unreleased/PR-*.md` fragment, classifies each
|
|
5
|
+
* against the project's persona roster + filter mode, and writes
|
|
6
|
+
* `<output_dir>/<tag>.md`. Atomic-rename pattern: the fragments dir is
|
|
7
|
+
* cleared only AFTER the compiled file lands on disk, so a crash mid-write
|
|
8
|
+
* cannot orphan fragments without their compiled output.
|
|
9
|
+
*
|
|
10
|
+
* Non-negotiable invariant (ID-6): unmapped fragments (no persona tag, or
|
|
11
|
+
* a tag the roster doesn't recognize) MUST appear in a `## Needs Triage`
|
|
12
|
+
* section. Never silent-drop. The triage section is the human curator's
|
|
13
|
+
* safety net — its presence is asserted by TD-3.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { loadProjectConfig } from '../config/load.mjs';
|
|
18
|
+
import { readPersonas } from '../personas/sidecar.mjs';
|
|
19
|
+
import { atomicWriteText } from '../util/atomic-write.mjs';
|
|
20
|
+
import { resolveOutputDir, validateTag } from './output-dir.mjs';
|
|
21
|
+
import { buildPersonaTierMap, classifyFragment, isNoneSentinel } from './persona-filter.mjs';
|
|
22
|
+
const VALID_MODES = new Set(['primary', 'primary+secondary', 'all']);
|
|
23
|
+
/** Pre-compiled at module level to avoid recompilation in the sort comparator (PRF-001). */
|
|
24
|
+
const PR_FILENAME_RE = /^PR-(\d+)\.md$/;
|
|
25
|
+
/**
|
|
26
|
+
* Parse a single fragment file. Tolerant: missing keys default to empty
|
|
27
|
+
* strings; the body after `## Release note` is read verbatim.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} text
|
|
30
|
+
* @returns {{ pr: string, date: string, persona: string, note: string, raw: string }}
|
|
31
|
+
*/
|
|
32
|
+
function parseFragment(text) {
|
|
33
|
+
const lines = text.split('\n');
|
|
34
|
+
let pr = '';
|
|
35
|
+
let date = '';
|
|
36
|
+
let persona = '';
|
|
37
|
+
let note = '';
|
|
38
|
+
let noteStart = -1;
|
|
39
|
+
for (let i = 0; i < lines.length; i++) {
|
|
40
|
+
const line = lines[i];
|
|
41
|
+
if (line.startsWith('pr: '))
|
|
42
|
+
pr = line.slice(4).trim();
|
|
43
|
+
else if (line.startsWith('date: '))
|
|
44
|
+
date = line.slice(6).trim();
|
|
45
|
+
else if (line.startsWith('persona: '))
|
|
46
|
+
persona = line.slice(9).trim();
|
|
47
|
+
else if (line.startsWith('## Release note')) {
|
|
48
|
+
noteStart = i;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (noteStart >= 0) {
|
|
53
|
+
note = lines
|
|
54
|
+
.slice(noteStart + 1)
|
|
55
|
+
.join('\n')
|
|
56
|
+
.trim();
|
|
57
|
+
}
|
|
58
|
+
return { pr, date, persona, note, raw: text };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the output dir and persona filter mode from config + flag override.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} projectDir
|
|
64
|
+
* @param {string | null} flagOverride
|
|
65
|
+
* @returns {{ outputDir: string, personaFilter: 'primary' | 'primary+secondary' | 'all' }}
|
|
66
|
+
*/
|
|
67
|
+
function resolveContext(projectDir, flagOverride) {
|
|
68
|
+
const config = loadProjectConfig(projectDir);
|
|
69
|
+
const outputDir = resolveOutputDir(projectDir, flagOverride);
|
|
70
|
+
const filterRaw = config?.changelog?.release_notes?.persona_filter;
|
|
71
|
+
const personaFilter = typeof filterRaw === 'string' && VALID_MODES.has(filterRaw)
|
|
72
|
+
? /** @type {'primary' | 'primary+secondary' | 'all'} */ (filterRaw)
|
|
73
|
+
: 'primary+secondary';
|
|
74
|
+
return { outputDir, personaFilter };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* @typedef {Object} CompileInput
|
|
78
|
+
* @property {string} projectDir
|
|
79
|
+
* @property {string} tag
|
|
80
|
+
* @property {string | null} outputDir
|
|
81
|
+
*/
|
|
82
|
+
/**
|
|
83
|
+
* @typedef {Object} CompileResult
|
|
84
|
+
* @property {boolean} written
|
|
85
|
+
* @property {string} path
|
|
86
|
+
* @property {number} fragmentsTotal
|
|
87
|
+
* @property {number} fragmentsInBody
|
|
88
|
+
* @property {number} fragmentsInTriage
|
|
89
|
+
* @property {number} fragmentsFiltered
|
|
90
|
+
* @property {boolean} clearedUnreleased
|
|
91
|
+
*/
|
|
92
|
+
/**
|
|
93
|
+
* Compile every `_unreleased/PR-*.md` fragment into `<output_dir>/<tag>.md`.
|
|
94
|
+
*
|
|
95
|
+
* @param {CompileInput} input
|
|
96
|
+
* @returns {CompileResult}
|
|
97
|
+
*/
|
|
98
|
+
export function compileTag(input) {
|
|
99
|
+
validateTag(input.tag);
|
|
100
|
+
const { outputDir, personaFilter } = resolveContext(input.projectDir, input.outputDir);
|
|
101
|
+
const fragmentsDir = join(outputDir, '_unreleased');
|
|
102
|
+
const targetPath = join(outputDir, `${input.tag}.md`);
|
|
103
|
+
const rawFiles = existsSync(fragmentsDir)
|
|
104
|
+
? readdirSync(fragmentsDir).filter((f) => PR_FILENAME_RE.test(f))
|
|
105
|
+
: [];
|
|
106
|
+
// Pre-compute PR numbers once to avoid re-running the regex in the comparator (PRF-001).
|
|
107
|
+
const withNumbers = rawFiles.map((f) => ({
|
|
108
|
+
f,
|
|
109
|
+
n: Number.parseInt(PR_FILENAME_RE.exec(f)?.[1] ?? '0', 10),
|
|
110
|
+
}));
|
|
111
|
+
withNumbers.sort((a, b) => a.n - b.n);
|
|
112
|
+
const fragments = withNumbers.map(({ f }) => ({
|
|
113
|
+
filename: f,
|
|
114
|
+
...parseFragment(readFileSync(join(fragmentsDir, f), 'utf-8')),
|
|
115
|
+
}));
|
|
116
|
+
const personasEnvelope = readPersonas(input.projectDir);
|
|
117
|
+
const tierMap = buildPersonaTierMap(personasEnvelope);
|
|
118
|
+
/** @type {Array<{ pr: string, date: string, persona: string, note: string, tier: 'primary' | 'secondary' | 'tertiary' }>} */
|
|
119
|
+
const bodyEntries = [];
|
|
120
|
+
/** @type {Array<{ pr: string, date: string, persona: string, note: string, reason: string }>} */
|
|
121
|
+
const triageEntries = [];
|
|
122
|
+
let filteredCount = 0;
|
|
123
|
+
for (const frag of fragments) {
|
|
124
|
+
const { bucket, resolvedTier } = classifyFragment(frag.persona, tierMap, personaFilter);
|
|
125
|
+
if (bucket === 'body' && resolvedTier !== null && resolvedTier !== 'unknown') {
|
|
126
|
+
bodyEntries.push({
|
|
127
|
+
pr: frag.pr,
|
|
128
|
+
date: frag.date,
|
|
129
|
+
persona: frag.persona,
|
|
130
|
+
note: frag.note,
|
|
131
|
+
tier: resolvedTier,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else if (bucket === 'triage' || resolvedTier === 'unknown') {
|
|
135
|
+
// Use the shared sentinel predicate so this label stays in lockstep
|
|
136
|
+
// with persona-filter.mjs's NONE_SENTINELS set. Without it, the
|
|
137
|
+
// classifier and labeller drift (e.g. literal "null" persona tags
|
|
138
|
+
// land in triage as 'unknown-persona' instead of 'no-persona-tag').
|
|
139
|
+
// 'unknown' tier (malformed tier value in personas.json) is also
|
|
140
|
+
// routed here — never silent-drop (ID-6 / LOG-001).
|
|
141
|
+
const reason = isNoneSentinel(frag.persona)
|
|
142
|
+
? 'no-persona-tag'
|
|
143
|
+
: resolvedTier === 'unknown'
|
|
144
|
+
? 'unknown-tier'
|
|
145
|
+
: 'unknown-persona';
|
|
146
|
+
triageEntries.push({
|
|
147
|
+
pr: frag.pr,
|
|
148
|
+
date: frag.date,
|
|
149
|
+
persona: frag.persona,
|
|
150
|
+
note: frag.note,
|
|
151
|
+
reason,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
filteredCount++;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const compiled = renderCompiled({
|
|
159
|
+
tag: input.tag,
|
|
160
|
+
body: bodyEntries,
|
|
161
|
+
triage: triageEntries,
|
|
162
|
+
filter: personaFilter,
|
|
163
|
+
});
|
|
164
|
+
atomicWriteText(targetPath, compiled);
|
|
165
|
+
// Clear _unreleased only AFTER the compiled file is durably written.
|
|
166
|
+
let clearedUnreleased = false;
|
|
167
|
+
if (existsSync(fragmentsDir) && fragments.length > 0) {
|
|
168
|
+
for (const frag of fragments) {
|
|
169
|
+
rmSync(join(fragmentsDir, frag.filename), { force: true });
|
|
170
|
+
}
|
|
171
|
+
clearedUnreleased = true;
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
written: true,
|
|
175
|
+
path: targetPath,
|
|
176
|
+
fragmentsTotal: fragments.length,
|
|
177
|
+
fragmentsInBody: bodyEntries.length,
|
|
178
|
+
fragmentsInTriage: triageEntries.length,
|
|
179
|
+
fragmentsFiltered: filteredCount,
|
|
180
|
+
clearedUnreleased,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Render the per-tag compiled body.
|
|
185
|
+
*
|
|
186
|
+
* Layout:
|
|
187
|
+
* # Release <tag>
|
|
188
|
+
*
|
|
189
|
+
* (per-tier sections — primary first, then secondary, then tertiary
|
|
190
|
+
* when filter==='all'. Each section's entries sorted by PR number.)
|
|
191
|
+
*
|
|
192
|
+
* ## Needs Triage (only when triage[] non-empty)
|
|
193
|
+
*
|
|
194
|
+
* @param {{
|
|
195
|
+
* tag: string,
|
|
196
|
+
* body: Array<{ pr: string, date: string, persona: string, note: string, tier: 'primary' | 'secondary' | 'tertiary' }>,
|
|
197
|
+
* triage: Array<{ pr: string, date: string, persona: string, note: string, reason: string }>,
|
|
198
|
+
* filter: 'primary' | 'primary+secondary' | 'all',
|
|
199
|
+
* }} input
|
|
200
|
+
* @returns {string}
|
|
201
|
+
*/
|
|
202
|
+
function renderCompiled(input) {
|
|
203
|
+
const lines = [];
|
|
204
|
+
lines.push(`# Release ${input.tag}`);
|
|
205
|
+
lines.push('');
|
|
206
|
+
lines.push(`<!-- persona_filter: ${input.filter} -->`);
|
|
207
|
+
lines.push('');
|
|
208
|
+
const tiersInOrder = /** @type {const} */ (['primary', 'secondary', 'tertiary']);
|
|
209
|
+
for (const tier of tiersInOrder) {
|
|
210
|
+
const rows = input.body.filter((e) => e.tier === tier);
|
|
211
|
+
if (rows.length === 0)
|
|
212
|
+
continue;
|
|
213
|
+
lines.push(`## ${capitalize(tier)} personas`);
|
|
214
|
+
lines.push('');
|
|
215
|
+
for (const row of rows) {
|
|
216
|
+
lines.push(`- **PR #${row.pr}** (${row.persona}): ${row.note}`);
|
|
217
|
+
}
|
|
218
|
+
lines.push('');
|
|
219
|
+
}
|
|
220
|
+
if (input.triage.length > 0) {
|
|
221
|
+
lines.push('## Needs Triage');
|
|
222
|
+
lines.push('');
|
|
223
|
+
lines.push('_These fragments lack a recognized persona tag. Curate before publishing._');
|
|
224
|
+
lines.push('');
|
|
225
|
+
for (const row of input.triage) {
|
|
226
|
+
const personaLabel = row.persona && row.persona !== '(none)' ? row.persona : '(no tag)';
|
|
227
|
+
lines.push(`- **PR #${row.pr}** (${personaLabel}, ${row.reason}): ${row.note}`);
|
|
228
|
+
}
|
|
229
|
+
lines.push('');
|
|
230
|
+
}
|
|
231
|
+
if (input.body.length === 0 && input.triage.length === 0) {
|
|
232
|
+
lines.push('_No fragments to compile._');
|
|
233
|
+
lines.push('');
|
|
234
|
+
}
|
|
235
|
+
return lines.join('\n');
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* @param {string} s
|
|
239
|
+
* @returns {string}
|
|
240
|
+
*/
|
|
241
|
+
function capitalize(s) {
|
|
242
|
+
return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=compile.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile.mjs","sourceRoot":"","sources":["../../../src/cli/release-notes/compile.mjs"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the canonical fragment body. The shape is intentionally simple +
|
|
3
|
+
* stable so the compile step can parse it back without YAML / front-matter
|
|
4
|
+
* gymnastics. Date is the calling day in UTC, not a wall-clock timestamp —
|
|
5
|
+
* idempotency demands the same inputs produce the same bytes regardless of
|
|
6
|
+
* when the day they're called within.
|
|
7
|
+
*
|
|
8
|
+
* @param {{
|
|
9
|
+
* pr: number,
|
|
10
|
+
* note: string,
|
|
11
|
+
* persona: string | null,
|
|
12
|
+
* date: string,
|
|
13
|
+
* commits: string[],
|
|
14
|
+
* }} input
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
export function renderFragment(input: {
|
|
18
|
+
pr: number;
|
|
19
|
+
note: string;
|
|
20
|
+
persona: string | null;
|
|
21
|
+
date: string;
|
|
22
|
+
commits: string[];
|
|
23
|
+
}): string;
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} DraftInput
|
|
26
|
+
* @property {string} projectDir
|
|
27
|
+
* @property {number} pr
|
|
28
|
+
* @property {string} note
|
|
29
|
+
* @property {string | null} persona
|
|
30
|
+
* @property {string | null} outputDir // CLI flag override (relative to projectDir or absolute).
|
|
31
|
+
* @property {string[] | null} commits // explicit commit list; null → autodetect via git log.
|
|
32
|
+
* @property {string | null} date // ISO yyyy-mm-dd; null → today (UTC).
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {Object} DraftResult
|
|
36
|
+
* @property {boolean} written
|
|
37
|
+
* @property {string} path
|
|
38
|
+
* @property {string} reason // 'created' | 'no-change' | 'updated'
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Write the PR fragment idempotently. Returns the result envelope.
|
|
42
|
+
*
|
|
43
|
+
* @param {DraftInput} input
|
|
44
|
+
* @returns {DraftResult}
|
|
45
|
+
*/
|
|
46
|
+
export function writeDraft(input: DraftInput): DraftResult;
|
|
47
|
+
export type DraftInput = {
|
|
48
|
+
projectDir: string;
|
|
49
|
+
pr: number;
|
|
50
|
+
note: string;
|
|
51
|
+
persona: string | null;
|
|
52
|
+
/**
|
|
53
|
+
* // CLI flag override (relative to projectDir or absolute).
|
|
54
|
+
*/
|
|
55
|
+
outputDir: string | null;
|
|
56
|
+
/**
|
|
57
|
+
* // explicit commit list; null → autodetect via git log.
|
|
58
|
+
*/
|
|
59
|
+
commits: string[] | null;
|
|
60
|
+
/**
|
|
61
|
+
* // ISO yyyy-mm-dd; null → today (UTC).
|
|
62
|
+
*/
|
|
63
|
+
date: string | null;
|
|
64
|
+
};
|
|
65
|
+
export type DraftResult = {
|
|
66
|
+
written: boolean;
|
|
67
|
+
path: string;
|
|
68
|
+
/**
|
|
69
|
+
* // 'created' | 'no-change' | 'updated'
|
|
70
|
+
*/
|
|
71
|
+
reason: string;
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=draft.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draft.d.mts","sourceRoot":"","sources":["../../../src/cli/release-notes/draft.mjs"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;;;;GAeG;AACH,sCATW;IACN,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,GACS,MAAM,CAmBlB;AA4BD;;;;;;;;;GASG;AAEH;;;;;GAKG;AAEH;;;;;GAKG;AACH,kCAHW,UAAU,GACR,WAAW,CA4BvB;;gBAhDa,MAAM;QACN,MAAM;UACN,MAAM;aACN,MAAM,GAAG,IAAI;;;;eACb,MAAM,GAAG,IAAI;;;;aACb,MAAM,EAAE,GAAG,IAAI;;;;UACf,MAAM,GAAG,IAAI;;;aAKb,OAAO;UACP,MAAM;;;;YACN,MAAM"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* release-notes/draft.mjs — towncrier-style per-PR fragment writer.
|
|
3
|
+
*
|
|
4
|
+
* Writes `<output_dir>/_unreleased/PR-<n>.md` for a single PR. Idempotent:
|
|
5
|
+
* if the target file already exists with byte-identical content, the call
|
|
6
|
+
* is a no-op and the envelope reports `written: false`. The same input
|
|
7
|
+
* always produces the same bytes on disk — that is the load-bearing
|
|
8
|
+
* invariant the news-fragment pattern depends on (zero merge conflicts
|
|
9
|
+
* on the unreleased file, AC2).
|
|
10
|
+
*
|
|
11
|
+
* Deterministic I/O only. No LLM calls. No prompt templates. The host
|
|
12
|
+
* LLM authors the note text and hands it in via --note.
|
|
13
|
+
*/
|
|
14
|
+
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { atomicWriteText } from '../util/atomic-write.mjs';
|
|
18
|
+
import { resolveOutputDir } from './output-dir.mjs';
|
|
19
|
+
/**
|
|
20
|
+
* Build the canonical fragment body. The shape is intentionally simple +
|
|
21
|
+
* stable so the compile step can parse it back without YAML / front-matter
|
|
22
|
+
* gymnastics. Date is the calling day in UTC, not a wall-clock timestamp —
|
|
23
|
+
* idempotency demands the same inputs produce the same bytes regardless of
|
|
24
|
+
* when the day they're called within.
|
|
25
|
+
*
|
|
26
|
+
* @param {{
|
|
27
|
+
* pr: number,
|
|
28
|
+
* note: string,
|
|
29
|
+
* persona: string | null,
|
|
30
|
+
* date: string,
|
|
31
|
+
* commits: string[],
|
|
32
|
+
* }} input
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
export function renderFragment(input) {
|
|
36
|
+
const personaLine = input.persona ? `persona: ${input.persona}` : 'persona: (none)';
|
|
37
|
+
const commitsLine = input.commits.length === 0 ? 'commits: (none)' : `commits: ${input.commits.join(', ')}`;
|
|
38
|
+
return [
|
|
39
|
+
`# PR-${input.pr}`,
|
|
40
|
+
'',
|
|
41
|
+
`date: ${input.date}`,
|
|
42
|
+
`pr: ${input.pr}`,
|
|
43
|
+
personaLine,
|
|
44
|
+
commitsLine,
|
|
45
|
+
'',
|
|
46
|
+
'## Release note',
|
|
47
|
+
'',
|
|
48
|
+
input.note.trim(),
|
|
49
|
+
'',
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the commits referencing a PR by number. Best-effort — if git is
|
|
54
|
+
* unavailable or no matching commits are found, returns an empty array.
|
|
55
|
+
* Caller can pass commits explicitly via --commits to bypass git inspection.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} projectDir
|
|
58
|
+
* @param {number} pr
|
|
59
|
+
* @returns {string[]}
|
|
60
|
+
*/
|
|
61
|
+
function discoverCommitsForPr(projectDir, pr) {
|
|
62
|
+
try {
|
|
63
|
+
const out = execFileSync('git', ['-C', projectDir, 'log', '--no-merges', `--grep=#${pr}\\b`, '--format=%H'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
64
|
+
return out
|
|
65
|
+
.split('\n')
|
|
66
|
+
.map((s) => s.trim())
|
|
67
|
+
.filter((s) => s.length > 0)
|
|
68
|
+
.map((sha) => sha.slice(0, 12));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* @typedef {Object} DraftInput
|
|
76
|
+
* @property {string} projectDir
|
|
77
|
+
* @property {number} pr
|
|
78
|
+
* @property {string} note
|
|
79
|
+
* @property {string | null} persona
|
|
80
|
+
* @property {string | null} outputDir // CLI flag override (relative to projectDir or absolute).
|
|
81
|
+
* @property {string[] | null} commits // explicit commit list; null → autodetect via git log.
|
|
82
|
+
* @property {string | null} date // ISO yyyy-mm-dd; null → today (UTC).
|
|
83
|
+
*/
|
|
84
|
+
/**
|
|
85
|
+
* @typedef {Object} DraftResult
|
|
86
|
+
* @property {boolean} written
|
|
87
|
+
* @property {string} path
|
|
88
|
+
* @property {string} reason // 'created' | 'no-change' | 'updated'
|
|
89
|
+
*/
|
|
90
|
+
/**
|
|
91
|
+
* Write the PR fragment idempotently. Returns the result envelope.
|
|
92
|
+
*
|
|
93
|
+
* @param {DraftInput} input
|
|
94
|
+
* @returns {DraftResult}
|
|
95
|
+
*/
|
|
96
|
+
export function writeDraft(input) {
|
|
97
|
+
const outputDir = resolveOutputDir(input.projectDir, input.outputDir);
|
|
98
|
+
const unreleasedDir = join(outputDir, '_unreleased');
|
|
99
|
+
const target = join(unreleasedDir, `PR-${input.pr}.md`);
|
|
100
|
+
const date = input.date ?? new Date().toISOString().slice(0, 10);
|
|
101
|
+
const commits = input.commits ?? discoverCommitsForPr(input.projectDir, input.pr);
|
|
102
|
+
const body = renderFragment({
|
|
103
|
+
pr: input.pr,
|
|
104
|
+
note: input.note,
|
|
105
|
+
persona: input.persona,
|
|
106
|
+
date,
|
|
107
|
+
commits,
|
|
108
|
+
});
|
|
109
|
+
if (existsSync(target)) {
|
|
110
|
+
const existing = readFileSync(target, 'utf-8');
|
|
111
|
+
if (existing === body) {
|
|
112
|
+
return { written: false, path: target, reason: 'no-change' };
|
|
113
|
+
}
|
|
114
|
+
atomicWriteText(target, body);
|
|
115
|
+
return { written: true, path: target, reason: 'updated' };
|
|
116
|
+
}
|
|
117
|
+
atomicWriteText(target, body);
|
|
118
|
+
return { written: true, path: target, reason: 'created' };
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=draft.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draft.mjs","sourceRoot":"","sources":["../../../src/cli/release-notes/draft.mjs"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate a release tag. Throws if the tag contains path-traversal
|
|
3
|
+
* sequences (`..`, `/`) or characters outside the semver-compatible
|
|
4
|
+
* allowlist. Defends path.join calls in compile.mjs and publish.mjs
|
|
5
|
+
* against directory escape (SEC-001).
|
|
6
|
+
*
|
|
7
|
+
* @param {string} tag
|
|
8
|
+
* @returns {void}
|
|
9
|
+
*/
|
|
10
|
+
export function validateTag(tag: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the canonical output directory from flag override → config → default.
|
|
13
|
+
* Flag override wins when present. Returns an absolute path.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} projectDir
|
|
16
|
+
* @param {string | null} flagOverride
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export function resolveOutputDir(projectDir: string, flagOverride: string | null): string;
|
|
20
|
+
//# sourceMappingURL=output-dir.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-dir.d.mts","sourceRoot":"","sources":["../../../src/cli/release-notes/output-dir.mjs"],"names":[],"mappings":"AAgBA;;;;;;;;GAQG;AACH,iCAHW,MAAM,GACJ,IAAI,CAWhB;AAED;;;;;;;GAOG;AACH,6CAJW,MAAM,gBACN,MAAM,GAAG,IAAI,GACX,MAAM,CASlB"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* release-notes/output-dir.mjs — shared output directory resolver.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from draft.mjs, compile.mjs, and publish.mjs to eliminate
|
|
5
|
+
* copy-paste duplication (QUA-003).
|
|
6
|
+
*/
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
import { loadProjectConfig } from '../config/load.mjs';
|
|
9
|
+
const DEFAULT_OUTPUT_DIR = 'docs/releases';
|
|
10
|
+
/** Allowlist for release tag values — semver-compatible subset. */
|
|
11
|
+
const TAG_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
12
|
+
/**
|
|
13
|
+
* Validate a release tag. Throws if the tag contains path-traversal
|
|
14
|
+
* sequences (`..`, `/`) or characters outside the semver-compatible
|
|
15
|
+
* allowlist. Defends path.join calls in compile.mjs and publish.mjs
|
|
16
|
+
* against directory escape (SEC-001).
|
|
17
|
+
*
|
|
18
|
+
* @param {string} tag
|
|
19
|
+
* @returns {void}
|
|
20
|
+
*/
|
|
21
|
+
export function validateTag(tag) {
|
|
22
|
+
if (tag.includes('..') || tag.includes('/')) {
|
|
23
|
+
throw new Error(`Invalid --tag: "${tag}" contains path-traversal sequences (.. or /)`);
|
|
24
|
+
}
|
|
25
|
+
if (!TAG_PATTERN.test(tag)) {
|
|
26
|
+
throw new Error(`Invalid --tag: "${tag}" must match /^[a-zA-Z0-9._-]+$/ (no spaces or special characters)`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the canonical output directory from flag override → config → default.
|
|
31
|
+
* Flag override wins when present. Returns an absolute path.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} projectDir
|
|
34
|
+
* @param {string | null} flagOverride
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function resolveOutputDir(projectDir, flagOverride) {
|
|
38
|
+
const config = loadProjectConfig(projectDir);
|
|
39
|
+
const configured = config?.changelog?.release_notes?.output_dir;
|
|
40
|
+
return resolve(projectDir, flagOverride ?? (typeof configured === 'string' ? configured : DEFAULT_OUTPUT_DIR));
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=output-dir.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-dir.mjs","sourceRoot":"","sources":["../../../src/cli/release-notes/output-dir.mjs"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAEtD,MAAM,kBAAkB,GAAG,eAAe,CAAA;AAE1C,mEAAmE;AACnE,MAAM,WAAW,GAAG,mBAAmB,CAAA;AAEvC;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,GAAG;IAC9B,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CAAC,mBAAmB,GAAG,+CAA+C,CAAC,CAAA;IACvF,CAAC;IACD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CACd,mBAAmB,GAAG,oEAAoE,CAC1F,CAAA;IACF,CAAC;AACF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAU,EAAE,YAAY;IACxD,MAAM,MAAM,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAA;IAC5C,MAAM,UAAU,GAAG,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,CAAA;IAC/D,OAAO,OAAO,CACb,UAAU,EACV,YAAY,IAAI,CAAC,OAAO,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAClF,CAAA;AACF,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when the raw persona-tag string from a fragment should be treated as
|
|
3
|
+
* "no tag" (and therefore routed to triage with reason `no-persona-tag`).
|
|
4
|
+
* Mirrors `NONE_SENTINELS` so callers — notably `compile.mjs`'s triage-reason
|
|
5
|
+
* labeller — stay in lockstep with the classifier. Case-insensitive.
|
|
6
|
+
*
|
|
7
|
+
* @param {string | null | undefined} raw
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function isNoneSentinel(raw: string | null | undefined): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Read tier + name out of a persona envelope row (the shape personas.mjs
|
|
13
|
+
* persists). Tier lives inside `ui_json` (a JSON string). Returns null
|
|
14
|
+
* when the row is unusable.
|
|
15
|
+
*
|
|
16
|
+
* @param {{ ui_json?: unknown }} row
|
|
17
|
+
* @returns {{ name: string, tier: PersonaTier } | null}
|
|
18
|
+
*/
|
|
19
|
+
export function extractPersonaIdentity(row: {
|
|
20
|
+
ui_json?: unknown;
|
|
21
|
+
}): {
|
|
22
|
+
name: string;
|
|
23
|
+
tier: PersonaTier;
|
|
24
|
+
} | null;
|
|
25
|
+
/**
|
|
26
|
+
* Build a name → tier map from a personas envelope.
|
|
27
|
+
*
|
|
28
|
+
* @param {{ personas?: Array<{ ui_json?: unknown }> } | null | undefined} envelope
|
|
29
|
+
* @returns {Map<string, PersonaTier>}
|
|
30
|
+
*/
|
|
31
|
+
export function buildPersonaTierMap(envelope: {
|
|
32
|
+
personas?: Array<{
|
|
33
|
+
ui_json?: unknown;
|
|
34
|
+
}>;
|
|
35
|
+
} | null | undefined): Map<string, PersonaTier>;
|
|
36
|
+
/**
|
|
37
|
+
* Classify a fragment's persona tag against the roster + filter mode.
|
|
38
|
+
*
|
|
39
|
+
* @param {string | null | undefined} personaTag The raw tag from the fragment.
|
|
40
|
+
* @param {Map<string, PersonaTier>} tierMap From `buildPersonaTierMap`.
|
|
41
|
+
* @param {'primary' | 'primary+secondary' | 'all'} mode
|
|
42
|
+
* @returns {{ bucket: FilterBucket, resolvedName: string | null, resolvedTier: PersonaTier | null }}
|
|
43
|
+
*/
|
|
44
|
+
export function classifyFragment(personaTag: string | null | undefined, tierMap: Map<string, PersonaTier>, mode: "primary" | "primary+secondary" | "all"): {
|
|
45
|
+
bucket: FilterBucket;
|
|
46
|
+
resolvedName: string | null;
|
|
47
|
+
resolvedTier: PersonaTier | null;
|
|
48
|
+
};
|
|
49
|
+
export type PersonaTier = "primary" | "secondary" | "tertiary" | "unknown";
|
|
50
|
+
export type FilterBucket = "body" | "triage" | "filtered";
|
|
51
|
+
//# sourceMappingURL=persona-filter.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persona-filter.d.mts","sourceRoot":"","sources":["../../../src/cli/release-notes/persona-filter.mjs"],"names":[],"mappings":"AAsCA;;;;;;;;GAQG;AACH,oCAHW,MAAM,GAAG,IAAI,GAAG,SAAS,GACvB,OAAO,CAKnB;AAED;;;;;;;GAOG;AACH,4CAHW;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GAAG,IAAI,CAmBtD;AAED;;;;;GAKG;AACH,8CAHW;IAAE,QAAQ,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GAC5D,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAYpC;AAED;;;;;;;GAOG;AACH,6CALW,MAAM,GAAG,IAAI,GAAG,SAAS,WACzB,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,QACxB,SAAS,GAAG,mBAAmB,GAAG,KAAK,GACrC;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,YAAY,EAAE,WAAW,GAAG,IAAI,CAAA;CAAE,CAsBnG;0BA5Fa,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS;2BAChD,MAAM,GAAG,QAAQ,GAAG,UAAU"}
|