contextdevkit 1.8.1 → 1.9.0
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/install.mjs +1 -1
- package/package.json +1 -1
- package/templates/CLAUDE.md.tpl +2 -1
- package/templates/claude/commands/README.md +6 -2
- package/templates/claude/commands/audit/validate-doc.md +37 -0
- package/templates/claude/commands/bug-hunt.md +10 -2
- package/templates/claude/commands/forge/forge-new.md +6 -0
- package/templates/claude/commands/pipeline/dev-start.md +21 -6
- package/templates/claude/commands/pipeline/pipeline.md +5 -1
- package/templates/claude/commands/pipeline/ship.md +6 -1
- package/templates/claude/commands/roadmap.md +10 -1
- package/templates/claude/commands/vcs/changelog-social.md +30 -0
- package/templates/claude/commands/vcs/draft-changelog.md +28 -0
- package/templates/claude/commands/vcs/gh-triage.md +41 -0
- package/templates/contextkit/policy/complexity-rubric.json +52 -0
- package/templates/contextkit/runtime/config/paths.mjs +2 -0
- package/templates/contextkit/tools/scripts/complexity-rubric.mjs +164 -0
- package/templates/contextkit/tools/scripts/draft-changelog.mjs +101 -0
- package/templates/contextkit/tools/scripts/validate-doc.mjs +135 -0
- package/tools/integration-test-tooling.mjs +25 -1
- package/tools/selfcheck-source.mjs +72 -0
- package/docs/CHANGELOG.md +0 -559
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `/draft-changelog` — build a `[Unreleased]` skeleton from Conventional Commits
|
|
4
|
+
* since the last tag (ADR-0030, OSS repo-ops).
|
|
5
|
+
*
|
|
6
|
+
* Reads `git log <lastTag>..HEAD`, parses Conventional Commit subjects, and groups
|
|
7
|
+
* them into Keep-a-Changelog sections (Added / Changed / Fixed / …). It DRAFTS —
|
|
8
|
+
* it never writes `docs/CHANGELOG.md`; the human reviews and pastes. Local git only
|
|
9
|
+
* (no network), every git call is timed out + defensive (rule 2): a non-repo or a
|
|
10
|
+
* git error prints a clean message, never a stack trace.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node contextkit/tools/scripts/draft-changelog.mjs # since last tag
|
|
14
|
+
* node contextkit/tools/scripts/draft-changelog.mjs --since v1.7.0
|
|
15
|
+
* node contextkit/tools/scripts/draft-changelog.mjs --json
|
|
16
|
+
*/
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
|
|
19
|
+
/** Conventional-commit type → Keep-a-Changelog section. */
|
|
20
|
+
const SECTION = {
|
|
21
|
+
feat: 'Added',
|
|
22
|
+
fix: 'Fixed',
|
|
23
|
+
perf: 'Changed',
|
|
24
|
+
refactor: 'Changed',
|
|
25
|
+
revert: 'Removed',
|
|
26
|
+
security: 'Security',
|
|
27
|
+
docs: 'Documentation',
|
|
28
|
+
chore: 'Chores',
|
|
29
|
+
build: 'Chores',
|
|
30
|
+
ci: 'Chores',
|
|
31
|
+
test: 'Chores',
|
|
32
|
+
style: 'Chores',
|
|
33
|
+
};
|
|
34
|
+
const ORDER = ['Added', 'Changed', 'Fixed', 'Removed', 'Deprecated', 'Security', 'Documentation', 'Chores', 'Other'];
|
|
35
|
+
|
|
36
|
+
/** Runs git with a hard timeout; returns stdout or null on any failure. */
|
|
37
|
+
function git(args) {
|
|
38
|
+
const res = spawnSync('git', args, { encoding: 'utf-8', timeout: 5000 });
|
|
39
|
+
return res.status === 0 ? (res.stdout || '').trim() : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function lastTag() {
|
|
43
|
+
return git(['describe', '--tags', '--abbrev=0']);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Parses a `feat(scope)!: subject` line into { section, scope, breaking, text }. */
|
|
47
|
+
function parse(subject) {
|
|
48
|
+
const m = subject.match(/^(\w+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/);
|
|
49
|
+
if (!m) return { section: 'Other', scope: null, breaking: false, text: subject };
|
|
50
|
+
const [, type, scope, bang, text] = m;
|
|
51
|
+
return { section: SECTION[type.toLowerCase()] || 'Other', scope: scope || null, breaking: Boolean(bang), text };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function collect(since) {
|
|
55
|
+
if (git(['rev-parse', '--is-inside-work-tree']) !== 'true') return { error: 'not a git repository' };
|
|
56
|
+
const range = since ? `${since}..HEAD` : null;
|
|
57
|
+
const args = ['log', '--no-merges', '--pretty=%s'];
|
|
58
|
+
if (range) args.splice(1, 0, range);
|
|
59
|
+
const out = git(args);
|
|
60
|
+
if (out === null) return { error: since ? `git log failed for range ${range}` : 'git log failed' };
|
|
61
|
+
const subjects = out.split('\n').filter(Boolean);
|
|
62
|
+
const groups = {};
|
|
63
|
+
for (const s of subjects) {
|
|
64
|
+
const p = parse(s);
|
|
65
|
+
(groups[p.section] ||= []).push(p);
|
|
66
|
+
}
|
|
67
|
+
return { since, count: subjects.length, groups };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function render(result) {
|
|
71
|
+
if (result.error) return `ℹ️ ${result.error} — nothing to draft.`;
|
|
72
|
+
const lines = ['## [Unreleased]', ''];
|
|
73
|
+
if (result.count === 0) {
|
|
74
|
+
lines.push(`_No commits since ${result.since || 'the start'}._`);
|
|
75
|
+
return lines.join('\n');
|
|
76
|
+
}
|
|
77
|
+
for (const section of ORDER) {
|
|
78
|
+
const items = result.groups[section];
|
|
79
|
+
if (!items || items.length === 0) continue;
|
|
80
|
+
lines.push(`### ${section}`);
|
|
81
|
+
for (const it of items) {
|
|
82
|
+
const scope = it.scope ? `**${it.scope}:** ` : '';
|
|
83
|
+
const breaking = it.breaking ? '⚠️ BREAKING — ' : '';
|
|
84
|
+
lines.push(`- ${breaking}${scope}${it.text}`);
|
|
85
|
+
}
|
|
86
|
+
lines.push('');
|
|
87
|
+
}
|
|
88
|
+
lines.push(`_Drafted from ${result.count} commit(s) since ${result.since || 'the first commit'}. Review before pasting into docs/CHANGELOG.md — this command never writes it._`);
|
|
89
|
+
return lines.join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function main() {
|
|
93
|
+
const argv = process.argv.slice(2);
|
|
94
|
+
const wantJson = argv.includes('--json');
|
|
95
|
+
const sinceIdx = argv.indexOf('--since');
|
|
96
|
+
const since = sinceIdx !== -1 && argv[sinceIdx + 1] ? argv[sinceIdx + 1] : lastTag();
|
|
97
|
+
const result = collect(since);
|
|
98
|
+
console.log(wantJson ? JSON.stringify(result, null, 2) : render(result));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
main();
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `/validate-doc` — quality gate for OUR OWN planning artifacts (ADR-0030).
|
|
4
|
+
*
|
|
5
|
+
* Adapts EVO-METHOD/BMAD's `steps-v` document-validation chain (MIT) to
|
|
6
|
+
* ContextDevKit's artifacts: an ADR must pose its problem, decide plainly, and own
|
|
7
|
+
* its trade-offs; a roadmap must be measurable, not aspirational. The kit already
|
|
8
|
+
* validates *code wiring* (selfcheck) — this validates the *prose* of the decisions
|
|
9
|
+
* those tests are built on.
|
|
10
|
+
*
|
|
11
|
+
* Report-only by design (constitution §8 — never a false "pass", but also never a
|
|
12
|
+
* push-blocker): it prints findings and exits non-zero only so CI *could* gate on
|
|
13
|
+
* it; the slash command is advisory. Zero runtime deps (rule 1).
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node contextkit/tools/scripts/validate-doc.mjs <file.md>
|
|
17
|
+
* node contextkit/tools/scripts/validate-doc.mjs <file.md> --json
|
|
18
|
+
* node contextkit/tools/scripts/validate-doc.mjs --adr <file.md> # force ADR rubric
|
|
19
|
+
* node contextkit/tools/scripts/validate-doc.mjs --roadmap <file.md> # force roadmap rubric
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
22
|
+
import { basename } from 'node:path';
|
|
23
|
+
|
|
24
|
+
/** Leftover template tokens that mean the artifact was never filled in. */
|
|
25
|
+
const PLACEHOLDERS = [/<short decision title>/i, /\bNNNN\b/, /YYYY-MM-DD/, /<who>/i, /<what we will do/i, /ADR-XXXX/];
|
|
26
|
+
/** Words that signal a real trade-off was considered (Consequences quality). */
|
|
27
|
+
const TRADEOFF_HINTS = /(trade-?off|negative|risk|harder|downside|we give up|cost)/i;
|
|
28
|
+
/** Tokens that make a roadmap line measurable rather than aspirational. */
|
|
29
|
+
const MEASURABLE_HINTS = /(\d|%|by\s+\w+|target|metric|KPI|reduce|increase|within|ship|release)/i;
|
|
30
|
+
|
|
31
|
+
/** Splits a markdown doc into `## Heading` → body. Zero-dep, defensive. */
|
|
32
|
+
function sections(text) {
|
|
33
|
+
const out = {};
|
|
34
|
+
const re = /^#{2,3}\s+(.+?)\s*$/gm;
|
|
35
|
+
const heads = [];
|
|
36
|
+
let m;
|
|
37
|
+
while ((m = re.exec(text))) heads.push({ title: m[1].trim(), start: m.index + m[0].length });
|
|
38
|
+
heads.forEach((h, i) => {
|
|
39
|
+
const end = i + 1 < heads.length ? heads[i + 1].start : text.length;
|
|
40
|
+
out[h.title.toLowerCase()] = text.slice(h.start, end).trim();
|
|
41
|
+
});
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Detects the rubric to apply from the path + content. */
|
|
46
|
+
function detectType(file, text) {
|
|
47
|
+
const name = basename(file).toLowerCase();
|
|
48
|
+
if (/\/decisions\//.test(file.replace(/\\/g, '/')) || /^\d{4}-/.test(name) || /\*\*Status\*\*/i.test(text)) return 'adr';
|
|
49
|
+
if (/roadmap/.test(name)) return 'roadmap';
|
|
50
|
+
return 'generic';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function err(code, message) {
|
|
54
|
+
return { level: 'error', code, message };
|
|
55
|
+
}
|
|
56
|
+
function warn(code, message) {
|
|
57
|
+
return { level: 'warn', code, message };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** ADR rubric — sections present, status valid, no placeholders, trade-offs owned. */
|
|
61
|
+
function validateAdr(text) {
|
|
62
|
+
const findings = [];
|
|
63
|
+
const sec = sections(text);
|
|
64
|
+
const has = (name) => Object.keys(sec).some((k) => k === name || k.startsWith(name));
|
|
65
|
+
for (const required of ['context', 'decision', 'consequences']) {
|
|
66
|
+
if (!has(required)) findings.push(err('MISSING_SECTION', `ADR is missing a "## ${required[0].toUpperCase() + required.slice(1)}" section.`));
|
|
67
|
+
}
|
|
68
|
+
if (!/\*\*status\*\*\s*:/i.test(text)) findings.push(err('NO_STATUS', 'ADR has no "**Status**:" line.'));
|
|
69
|
+
else if (!/\*\*status\*\*\s*:\s*(proposed|accepted|superseded)/i.test(text)) findings.push(warn('STATUS_VALUE', 'Status is not one of Proposed / Accepted / Superseded.'));
|
|
70
|
+
for (const re of PLACEHOLDERS) {
|
|
71
|
+
if (re.test(text)) findings.push(err('PLACEHOLDER', `Unfilled template placeholder still present: ${re}`));
|
|
72
|
+
}
|
|
73
|
+
const context = sec['context'] || '';
|
|
74
|
+
if (context && context.length < 200) findings.push(warn('THIN_CONTEXT', `Context is thin (${context.length} chars) — it should state the forces WITHOUT already giving the answer.`));
|
|
75
|
+
const consequences = Object.entries(sec).find(([k]) => k.startsWith('consequences'))?.[1] || '';
|
|
76
|
+
if (consequences && !TRADEOFF_HINTS.test(consequences)) findings.push(warn('NO_TRADEOFFS', 'Consequences states no trade-off / negative / risk — a decision with only upsides is usually under-examined.'));
|
|
77
|
+
if (!/follow-?up/i.test(text)) findings.push(warn('NO_FOLLOWUPS', 'No follow-ups noted — what does this decision obligate next?'));
|
|
78
|
+
return findings;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Roadmap rubric — items exist and read as measurable, not aspirational. */
|
|
82
|
+
function validateRoadmap(text) {
|
|
83
|
+
const findings = [];
|
|
84
|
+
const items = text.split('\n').filter((l) => /^\s*[-*]\s+/.test(l) || /^\s*\d+\.\s+/.test(l));
|
|
85
|
+
if (items.length === 0) findings.push(warn('NO_ITEMS', 'No list items detected — is the roadmap populated?'));
|
|
86
|
+
const vague = items.filter((l) => l.replace(/^\s*[-*\d.]+\s*/, '').length > 12 && !MEASURABLE_HINTS.test(l));
|
|
87
|
+
if (items.length && vague.length / items.length > 0.5) {
|
|
88
|
+
findings.push(warn('NOT_MEASURABLE', `${vague.length}/${items.length} items read as aspirational (no number / date / target). Make outcomes measurable.`));
|
|
89
|
+
}
|
|
90
|
+
for (const re of PLACEHOLDERS) {
|
|
91
|
+
if (re.test(text)) findings.push(warn('PLACEHOLDER', `Template placeholder still present: ${re}`));
|
|
92
|
+
}
|
|
93
|
+
return findings;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validate(file, forced) {
|
|
97
|
+
const text = readFileSync(file, 'utf-8').replace(/^/, '');
|
|
98
|
+
const type = forced || detectType(file, text);
|
|
99
|
+
const findings = type === 'adr' ? validateAdr(text) : type === 'roadmap' ? validateRoadmap(text) : validateRoadmap(text);
|
|
100
|
+
return { file, type, findings };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function main() {
|
|
104
|
+
const argv = process.argv.slice(2);
|
|
105
|
+
const wantJson = argv.includes('--json');
|
|
106
|
+
let forced = null;
|
|
107
|
+
if (argv.includes('--adr')) forced = 'adr';
|
|
108
|
+
if (argv.includes('--roadmap')) forced = 'roadmap';
|
|
109
|
+
const file = argv.find((a) => !a.startsWith('--'));
|
|
110
|
+
if (!file) {
|
|
111
|
+
console.error('Usage: validate-doc.mjs <file.md> [--adr|--roadmap] [--json]');
|
|
112
|
+
process.exit(2);
|
|
113
|
+
}
|
|
114
|
+
if (!existsSync(file)) {
|
|
115
|
+
console.error(`File not found: ${file}`);
|
|
116
|
+
process.exit(2);
|
|
117
|
+
}
|
|
118
|
+
const report = validate(file, forced);
|
|
119
|
+
const errors = report.findings.filter((f) => f.level === 'error');
|
|
120
|
+
const warns = report.findings.filter((f) => f.level === 'warn');
|
|
121
|
+
|
|
122
|
+
if (wantJson) {
|
|
123
|
+
console.log(JSON.stringify({ ...report, errorCount: errors.length, warnCount: warns.length }, null, 2));
|
|
124
|
+
} else {
|
|
125
|
+
console.log(`\n📋 validate-doc — ${basename(file)} (${report.type} rubric)`);
|
|
126
|
+
console.log('─'.repeat(56));
|
|
127
|
+
if (report.findings.length === 0) console.log(' ✅ No issues — the artifact passes the rubric.');
|
|
128
|
+
for (const f of errors) console.log(` ❌ [${f.code}] ${f.message}`);
|
|
129
|
+
for (const f of warns) console.log(` ⚠️ [${f.code}] ${f.message}`);
|
|
130
|
+
console.log(`\n ${errors.length} error(s), ${warns.length} warning(s). (advisory — never blocks a push)`);
|
|
131
|
+
}
|
|
132
|
+
process.exit(errors.length > 0 ? 1 : 0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
main();
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
23
23
|
import { tmpdir } from 'node:os';
|
|
24
24
|
import { join } from 'node:path';
|
|
25
|
-
import { KIT, run, readJson, reporter, installFixture } from './it-helpers.mjs';
|
|
25
|
+
import { KIT, run, git, readJson, reporter, installFixture } from './it-helpers.mjs';
|
|
26
26
|
|
|
27
27
|
const rep = reporter();
|
|
28
28
|
const { ok, bad } = rep;
|
|
@@ -48,6 +48,30 @@ try {
|
|
|
48
48
|
(() => { try { const g = JSON.parse(gitStatus.stdout); return g.isRepo === true && g.remoteUrl === null; } catch { return false; } })()
|
|
49
49
|
? ok('git.mjs reports repo + missing remote') : bad(`git.mjs failed: ${gitStatus.stdout || gitStatus.stderr}`);
|
|
50
50
|
|
|
51
|
+
// ADR-0030 — complexity rubric: regulated domain auto-routes + forces architectural tier.
|
|
52
|
+
const clsLgpd = script('complexity-rubric.mjs', 'classify', 'store user CPF and consent', '--json');
|
|
53
|
+
(() => { try { const j = JSON.parse(clsLgpd.stdout); return j.domain === 'lgpd' && j.requiredAgents.includes('privacy-lgpd') && j.tier === 'architectural' && j.needsAdr === true; } catch { return false; } })()
|
|
54
|
+
? ok('complexity-rubric routes a regulated (LGPD) task to privacy-lgpd + architectural tier')
|
|
55
|
+
: bad(`complexity-rubric LGPD classify failed: ${clsLgpd.stdout || clsLgpd.stderr}`);
|
|
56
|
+
const clsTrivial = script('complexity-rubric.mjs', 'classify', 'fix typo in readme', '--json');
|
|
57
|
+
(() => { try { const j = JSON.parse(clsTrivial.stdout); return j.tier === 'trivial' && j.needsAdr === false && j.domain === 'general'; } catch { return false; } })()
|
|
58
|
+
? ok('complexity-rubric classifies a trivial task with no ceremony')
|
|
59
|
+
: bad(`complexity-rubric trivial classify failed: ${clsTrivial.stdout || clsTrivial.stderr}`);
|
|
60
|
+
|
|
61
|
+
// ADR-0030 — validate-doc flags an unfilled ADR template (placeholders), runs the adr rubric.
|
|
62
|
+
const vdTpl = script('validate-doc.mjs', 'contextkit/memory/decisions/_TEMPLATE.md', '--json');
|
|
63
|
+
(() => { try { const j = JSON.parse(vdTpl.stdout); return j.type === 'adr' && j.errorCount > 0 && j.findings.some((f) => f.code === 'PLACEHOLDER'); } catch { return false; } })()
|
|
64
|
+
? ok('validate-doc flags an unfilled ADR template (placeholders)')
|
|
65
|
+
: bad(`validate-doc template check failed: ${vdTpl.stdout || vdTpl.stderr}`);
|
|
66
|
+
|
|
67
|
+
// ADR-0030 — draft-changelog groups Conventional Commits since the last tag.
|
|
68
|
+
git(['add', '-A'], proj);
|
|
69
|
+
git(['commit', '-m', 'feat(x): add a thing', '--no-verify'], proj);
|
|
70
|
+
const dc = script('draft-changelog.mjs', '--json');
|
|
71
|
+
(() => { try { const j = JSON.parse(dc.stdout); return Array.isArray(j.groups?.Added) && j.groups.Added.some((i) => i.text.includes('add a thing')); } catch { return false; } })()
|
|
72
|
+
? ok('draft-changelog groups Conventional Commits into Keep-a-Changelog sections')
|
|
73
|
+
: bad(`draft-changelog failed: ${dc.stdout || dc.stderr}`);
|
|
74
|
+
|
|
51
75
|
// DevPipeline tests live in `integration-test-tooling-pipeline.mjs` (sibling).
|
|
52
76
|
|
|
53
77
|
// Deep analysis: aggregates the deterministic scanners into one report.
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* Every function takes the reporter `rep` ({ ok, bad }) plus only what it
|
|
19
19
|
* needs. Entry point: `runSourceChecks(rep, ctx)` where `ctx = { KIT }`.
|
|
20
20
|
*/
|
|
21
|
+
import { existsSync } from 'node:fs';
|
|
21
22
|
import { readFile, readdir } from 'node:fs/promises';
|
|
22
23
|
import { resolve } from 'node:path';
|
|
23
24
|
|
|
@@ -268,6 +269,38 @@ async function checkSourceInvariants(rep, KIT) {
|
|
|
268
269
|
['/ship scopes via the ADR catalog (ADR-0027)', 'templates/claude/commands/pipeline/ship.md', /adr-digest\.mjs/],
|
|
269
270
|
['/new-adr checks for an existing decision first (ADR-0027)', 'templates/claude/commands/new-adr.md', /adr-digest\.mjs/],
|
|
270
271
|
['/deep-analysis scans existing ADRs before drafting (ADR-0027)', 'templates/claude/commands/audit/deep-analysis.md', /adr-digest\.mjs/],
|
|
272
|
+
// ADR-0030 — per-task complexity rubric (EVO-METHOD/BMAD-derived, MIT).
|
|
273
|
+
['complexity-rubric loader exports classify (ADR-0030)', 'templates/contextkit/tools/scripts/complexity-rubric.mjs', /export function classify/],
|
|
274
|
+
['complexity-rubric loader exports loadRubric', 'templates/contextkit/tools/scripts/complexity-rubric.mjs', /export function loadRubric/],
|
|
275
|
+
['complexity-rubric falls back to an embedded default (never throws)', 'templates/contextkit/tools/scripts/complexity-rubric.mjs', /DEFAULT_RUBRIC/],
|
|
276
|
+
['complexity-rubric single-sources the path via pathsFor (rule 4)', 'templates/contextkit/tools/scripts/complexity-rubric.mjs', /pathsFor\(root\)\.complexityRubric/],
|
|
277
|
+
['rubric seed declares the lgpd domain → privacy-lgpd (ADR-0030)', 'templates/contextkit/policy/complexity-rubric.json', /"lgpd":[\s\S]*"privacy-lgpd"/],
|
|
278
|
+
['rubric seed declares the three ceremony tiers', 'templates/contextkit/policy/complexity-rubric.json', /"trivial":[\s\S]*"feature":[\s\S]*"architectural":/],
|
|
279
|
+
['paths.mjs exposes complexityRubric (ADR-0030)', 'templates/contextkit/runtime/config/paths.mjs', /complexityRubric:/],
|
|
280
|
+
['/dev-start right-sizes via the complexity rubric (ADR-0030)', 'templates/claude/commands/pipeline/dev-start.md', /complexity-rubric\.mjs classify/],
|
|
281
|
+
['/dev-start has a correct-course checkpoint (ADR-0030)', 'templates/claude/commands/pipeline/dev-start.md', /Correct-course checkpoint/],
|
|
282
|
+
['/ship right-sizes via the complexity rubric (ADR-0030)', 'templates/claude/commands/pipeline/ship.md', /complexity-rubric\.mjs classify/],
|
|
283
|
+
['/pipeline right-sizes a new task (ADR-0030)', 'templates/claude/commands/pipeline/pipeline.md', /complexity-rubric\.mjs classify/],
|
|
284
|
+
['installer seeds the complexity rubric (ADR-0030)', 'install.mjs', /policy\/complexity-rubric\.json/],
|
|
285
|
+
['installer seeds review-protocol.md — closes ADR-0029 gap (ADR-0030)', 'install.mjs', /'review-protocol\.md'/],
|
|
286
|
+
// ADR-0030 — document-quality validation (EVO steps-v adaptation, MIT).
|
|
287
|
+
['validate-doc validates ADR sections (ADR-0030)', 'templates/contextkit/tools/scripts/validate-doc.mjs', /function validateAdr/],
|
|
288
|
+
['validate-doc flags template placeholders', 'templates/contextkit/tools/scripts/validate-doc.mjs', /PLACEHOLDERS/],
|
|
289
|
+
['validate-doc checks consequences own a trade-off', 'templates/contextkit/tools/scripts/validate-doc.mjs', /TRADEOFF_HINTS/],
|
|
290
|
+
['validate-doc is advisory — never blocks (rule 8)', 'templates/contextkit/tools/scripts/validate-doc.mjs', /never blocks a push/],
|
|
291
|
+
['/validate-doc command briefing ships (ADR-0030)', 'templates/claude/commands/audit/validate-doc.md', /document-quality rubric/],
|
|
292
|
+
// ADR-0030 — OSS repo-ops (gh-triage / draft-changelog / changelog-social + RCA).
|
|
293
|
+
['draft-changelog groups Conventional Commits (ADR-0030)', 'templates/contextkit/tools/scripts/draft-changelog.mjs', /const SECTION = \{/],
|
|
294
|
+
['draft-changelog times out git calls (rule 2)', 'templates/contextkit/tools/scripts/draft-changelog.mjs', /timeout:\s*\d/],
|
|
295
|
+
['draft-changelog never writes the file (drafts only)', 'templates/contextkit/tools/scripts/draft-changelog.mjs', /never writes/],
|
|
296
|
+
['/draft-changelog command briefing ships', 'templates/claude/commands/vcs/draft-changelog.md', /Draft a \[Unreleased\]/i],
|
|
297
|
+
['/gh-triage classifies via the complexity rubric (ADR-0030)', 'templates/claude/commands/vcs/gh-triage.md', /complexity-rubric\.mjs classify/],
|
|
298
|
+
['/gh-triage degrades cleanly without gh (rule 8)', 'templates/claude/commands/vcs/gh-triage.md', /skip, never fake/],
|
|
299
|
+
['/changelog-social drafts only — never posts', 'templates/claude/commands/vcs/changelog-social.md', /never posts/i],
|
|
300
|
+
['bug-hunt emits a structured RCA writeup (ADR-0030)', 'templates/claude/commands/bug-hunt.md', /root-cause analysis/i],
|
|
301
|
+
// ADR-0030 — mid-flight elicitation (advanced-elicitation + correct-course).
|
|
302
|
+
['/roadmap new does advanced elicitation (ADR-0030)', 'templates/claude/commands/roadmap.md', /Advanced elicitation/],
|
|
303
|
+
['/forge-new does advanced elicitation (ADR-0030)', 'templates/claude/commands/forge/forge-new.md', /Advanced elicitation/],
|
|
271
304
|
];
|
|
272
305
|
for (const [label, rel, re] of cases) {
|
|
273
306
|
re.test(await srcText(rel)) ? ok(label) : bad(`${label} — pattern ${re} missing in ${rel}`);
|
|
@@ -318,9 +351,48 @@ async function checkWorkflowsPinned(rep, KIT) {
|
|
|
318
351
|
? ok('ci.yml declares least-privilege permissions (contents: read)') : bad('ci.yml missing contents:read permissions');
|
|
319
352
|
}
|
|
320
353
|
|
|
354
|
+
/**
|
|
355
|
+
* ADR-0030 — cross-doc link integrity. Scans the seeded top-level engine docs for
|
|
356
|
+
* relative markdown links to other `.md` files and asserts each target exists, so a
|
|
357
|
+
* deleted/renamed doc (the kind of rot the `review-protocol.md` seed gap caused)
|
|
358
|
+
* fails the build instead of shipping a dangling link. Scoped to the controlled set
|
|
359
|
+
* `templates/contextkit/*.md`; widening to `docs/` is an ADR-0030 follow-up.
|
|
360
|
+
*/
|
|
361
|
+
async function checkDocLinks(rep, KIT) {
|
|
362
|
+
const { ok, bad } = rep;
|
|
363
|
+
console.log('Checking cross-doc markdown links resolve (ADR-0030)...');
|
|
364
|
+
const dir = resolve(KIT, 'templates/contextkit');
|
|
365
|
+
const linkRe = /\[[^\]]*\]\(([^)]+)\)/g;
|
|
366
|
+
const offenders = [];
|
|
367
|
+
let checked = 0;
|
|
368
|
+
let entries = [];
|
|
369
|
+
try {
|
|
370
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
371
|
+
} catch {
|
|
372
|
+
/* no dir — nothing to check */
|
|
373
|
+
}
|
|
374
|
+
for (const e of entries) {
|
|
375
|
+
if (!e.isFile() || !e.name.endsWith('.md')) continue;
|
|
376
|
+
const text = await readFile(resolve(dir, e.name), 'utf-8').catch(() => '');
|
|
377
|
+
let m;
|
|
378
|
+
while ((m = linkRe.exec(text))) {
|
|
379
|
+
let target = m[1].trim().split(/\s+/)[0]; // drop an optional "title"
|
|
380
|
+
if (!target || target.startsWith('http') || target.startsWith('#') || target.startsWith('<')) continue;
|
|
381
|
+
target = target.split('#')[0]; // strip an anchor
|
|
382
|
+
if (!target.endsWith('.md')) continue;
|
|
383
|
+
checked += 1;
|
|
384
|
+
if (!existsSync(resolve(dir, target))) offenders.push(`${e.name} → ${target}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
offenders.length === 0
|
|
388
|
+
? ok(`cross-doc markdown links resolve (${checked} checked)`)
|
|
389
|
+
: offenders.forEach((o) => bad(`dangling doc link: ${o}`));
|
|
390
|
+
}
|
|
391
|
+
|
|
321
392
|
/** Runs every source/structural check in order. `ctx` = { KIT }. */
|
|
322
393
|
export async function runSourceChecks(rep, { KIT }) {
|
|
323
394
|
await checkSourceInvariants(rep, KIT);
|
|
324
395
|
await checkNoHardcodedPaths(rep, KIT);
|
|
325
396
|
await checkWorkflowsPinned(rep, KIT);
|
|
397
|
+
await checkDocLinks(rep, KIT);
|
|
326
398
|
}
|