Repository navigation
Expand file tree
/
Copy pathcheck-skill-cheatsheet.mjs
More file actions
283 lines (256 loc) · 10.5 KB
/
Copy pathcheck-skill-cheatsheet.mjs
File metadata and controls
283 lines (256 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
#!/usr/bin/env node
// Cheat-sheet conformance check — diffs the onboarding Skill's embedded
// cheat sheet against the canonical notation catalogue.
//
// Ground truth: notations/README.md §"Views" — the canonical index of the
// view notations (short name + file extension + status). Element notations
// live under §"Elements" and are out of scope for the cheat sheet check.
//
// What we check:
// A. Every notation in the canon catalogue has a row in the Skill's
// family-selection matrix carrying the canonical file extension.
// B. Every notation in the canon catalogue has a view-template at the
// canonical path `templates/<short>.<short>.transitrix.yaml`.
// C. Every file extension that appears in the Skill's family-selection
// matrix corresponds to a notation in the canon (no Skill-only rows).
// D. Every view-template path in the Skill's templates table corresponds
// to a notation in the canon (no Skill-only templates).
//
// What we do NOT check (out of scope for v1):
// - The codex zone-primitives section in the cheat sheet (codex is not in
// the catalogue table; it is a parallel zone-primitive notation).
// - Root-scaffolding templates (transitrix.yaml, AGENTS.md, copilot-
// instructions.md) — adopter-shape concerns, not notation-shape concerns.
// - Intra-spec drift (each spec's front-matter `file_extension` vs its
// "File header" section) — a separate known issue, not the Skill's fault.
// - The free-text "Situation" prose in the family-selection matrix.
//
// Exit codes:
// 0 — no drift
// 1 — drift found (one or more checks failed)
// 2 — script-internal error (file missing, parse failure)
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const REPO_ROOT = join(dirname(__filename), '..');
const CATALOGUE_PATH = join(REPO_ROOT, 'notations', 'README.md');
const SKILL_PATH = join(REPO_ROOT, 'transitrix', 'skills', 'onboard', 'SKILL.md');
// --- Parsers ---------------------------------------------------------------
// Pull the catalogue tables from notations/README.md.
// Returns: Map<short_name, { extension, status }>.
async function parseCatalogue() {
const text = await readFile(CATALOGUE_PATH, 'utf8');
const lines = text.split('\n');
// Locate the section heading.
const headingIdx = lines.findIndex(l => /^##\s+Views\s*$/.test(l));
if (headingIdx < 0) {
throw new Error(`notations/README.md: "## Views" section not found`);
}
// The section is split by class into subsections (diagrams/reports/
// documents), each with its own table — collect every table found before
// the next "## " heading, not just the first.
let sectionEnd = lines.length;
for (let i = headingIdx + 1; i < lines.length; i++) {
if (/^##\s/.test(lines[i])) {
sectionEnd = i;
break;
}
}
const out = new Map();
let sawTable = false;
for (let i = headingIdx + 1; i < sectionEnd; i++) {
if (!lines[i].startsWith('| Spec ')) continue;
sawTable = true;
// Data rows start two lines after the header (skip header + separator).
for (let j = i + 2; j < sectionEnd; j++) {
const line = lines[j];
if (!line.startsWith('|')) break;
// Structural separator row marking a sub-block (e.g.
// "| — | **Report views (C = 4)** | | | |") — not a notation entry.
if (/^\|\s+—\s+\|/.test(line)) continue;
const cells = line.split('|').map(c => c.trim());
// cells: ['', Spec, ShortName, Purpose, FileExtension, Status, '']
if (cells.length < 7) continue;
const shortNameCell = cells[2];
const extCell = cells[4];
const statusCell = cells[5];
const shortMatch = shortNameCell.match(/`([^`]+)`/);
const extMatch = extCell.match(/`([^`]+)`/);
if (!shortMatch || !extMatch) {
throw new Error(`notations/README.md: cannot parse catalogue row: ${line}`);
}
out.set(shortMatch[1], { extension: extMatch[1], status: statusCell });
}
}
if (!sawTable) {
throw new Error(`notations/README.md: catalogue table header not found`);
}
if (out.size === 0) {
throw new Error(`notations/README.md: catalogue table parsed empty`);
}
return out;
}
// Pull the family-selection matrix from transitrix/skills/onboard/SKILL.md.
// Returns: Set<file_extension>. The matrix has one row per notation; we use
// the file-extension column as the join key against the canon catalogue.
async function parseSkillMatrix() {
const text = await readFile(SKILL_PATH, 'utf8');
const lines = text.split('\n');
const headingIdx = lines.findIndex(l => /^###\s+Family selection\s*$/.test(l));
if (headingIdx < 0) {
throw new Error(`SKILL.md: "### Family selection" section not found`);
}
// The first markdown table after the heading is the matrix.
let headerIdx = -1;
for (let i = headingIdx + 1; i < lines.length; i++) {
if (lines[i].startsWith('| Situation ')) {
headerIdx = i;
break;
}
if (/^##\s|^###\s/.test(lines[i])) break;
}
if (headerIdx < 0) {
throw new Error(`SKILL.md: family-selection matrix header not found`);
}
const out = new Set();
for (let i = headerIdx + 2; i < lines.length; i++) {
const line = lines[i];
if (!line.startsWith('|')) break;
const cells = line.split('|').map(c => c.trim());
if (cells.length < 5) continue;
const extCell = cells[3];
const extMatch = extCell.match(/`([^`]+)`/);
if (!extMatch) {
throw new Error(`SKILL.md: cannot parse matrix row: ${line}`);
}
out.add(extMatch[1]);
}
if (out.size === 0) {
throw new Error(`SKILL.md: family-selection matrix parsed empty`);
}
return out;
}
// Pull the view-notation template paths from transitrix/skills/onboard/SKILL.md.
// Returns: Set<short_name> for every `templates/<notation>.<extShort>.transitrix.yaml`
// entry where <extShort> is the canonical extension short for <notation> per the
// catalogue (e.g. goals.dgca, activities.dgca for the DGCA family).
// Other template categories (root scaffolding, codex) don't match the shape and
// are naturally filtered out.
async function parseSkillTemplates(notationToExtShort) {
const text = await readFile(SKILL_PATH, 'utf8');
const re = /`templates\/([a-z][a-z0-9-]*)\.([a-z][a-z0-9-]*)\.transitrix\.yaml`/g;
const out = new Set();
let m;
while ((m = re.exec(text)) !== null) {
const [, notationShort, extShort] = m;
if (notationToExtShort.get(notationShort) === extShort) out.add(notationShort);
}
if (out.size === 0) {
throw new Error(`SKILL.md: no view-notation template paths found`);
}
return out;
}
// --- Reporter --------------------------------------------------------------
function reportFailure(failures) {
console.error(`\nSkill cheat-sheet drifted from the canon — ${failures.length} finding(s):\n`);
for (const f of failures) {
console.error(` - [${f.check}] ${f.message}`);
}
console.error(
`\nFix the Skill (transitrix/skills/onboard/SKILL.md) to match the canon ` +
`(notations/README.md), or — if the canon is wrong — update both ` +
`together in the same PR.\n`
);
}
// --- Main ------------------------------------------------------------------
async function main() {
let catalogue, skillMatrixExts, skillTemplateShorts;
try {
catalogue = await parseCatalogue();
// Build notation → extShort map (e.g. goals → 'dgca', bpmn → 'bpmn').
const notationToExtShort = new Map(
[...catalogue].map(([short, { extension }]) => [
short,
extension.replace(/^\*\./, '').replace(/\.transitrix\.yaml$/, ''),
])
);
[skillMatrixExts, skillTemplateShorts] = await Promise.all([
parseSkillMatrix(),
parseSkillTemplates(notationToExtShort),
]);
} catch (e) {
console.error(`error: ${e.message}`);
process.exit(2);
}
const failures = [];
// Build the canonical extension set + per-extension short-name lookup.
const canonExts = new Set();
const extToShort = new Map();
for (const [shortName, { extension }] of catalogue) {
canonExts.add(extension);
extToShort.set(extension, shortName);
}
// Check A: every catalogue notation has a matching row in the matrix.
for (const [shortName, { extension }] of catalogue) {
if (!skillMatrixExts.has(extension)) {
failures.push({
check: 'A',
message:
`notation "${shortName}" (extension \`${extension}\`) is in the canon catalogue ` +
`(notations/README.md) but has no row in the Skill's family-selection matrix ` +
`(SKILL.md §"Family selection") carrying that extension.`,
});
}
}
// Check B: every catalogue notation has a matching template at the canonical path.
for (const [shortName, { extension }] of catalogue) {
if (!skillTemplateShorts.has(shortName)) {
const extShort = extension.replace(/^\*\./, '').replace(/\.transitrix\.yaml$/, '');
failures.push({
check: 'B',
message:
`notation "${shortName}" is in the canon catalogue but has no view-template ` +
`entry at \`templates/${shortName}.${extShort}.transitrix.yaml\` in the ` +
`Skill's templates table.`,
});
}
}
// Check C: every extension in the matrix is in the canon catalogue.
for (const ext of skillMatrixExts) {
if (!canonExts.has(ext)) {
failures.push({
check: 'C',
message:
`extension \`${ext}\` appears in the Skill's family-selection matrix but is ` +
`not in the canon catalogue (notations/README.md). Either the cheat-sheet has ` +
`a stray row or the catalogue is missing the notation.`,
});
}
}
// Check D: every view-template short name in the Skill is in the catalogue.
for (const shortName of skillTemplateShorts) {
if (!catalogue.has(shortName)) {
failures.push({
check: 'D',
message:
`view-template \`templates/${shortName}.${shortName}.transitrix.yaml\` is ` +
`listed in the Skill's templates table but "${shortName}" is not a notation ` +
`in the canon catalogue (notations/README.md).`,
});
}
}
if (failures.length > 0) {
reportFailure(failures);
process.exit(1);
}
console.log(
`Skill cheat-sheet conforms to the canon — ${catalogue.size} notations ` +
`checked across the family-selection matrix and the view-templates table.`
);
process.exit(0);
}
main().catch(e => {
console.error(`error: ${e.message}`);
process.exit(2);
});