@marcomigozzi/trailhead 0.16.0 → 0.17.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/README.it.md +67 -18
- package/README.md +67 -18
- package/bin/capture-action.test.js +167 -0
- package/bin/capture-docs.test.js +32 -0
- package/bin/config-dir-templates-docs.test.js +52 -0
- package/bin/decision-builds-docs.test.js +75 -0
- package/bin/engine-route-docs.test.js +148 -0
- package/bin/fog-vs-ticket-docs.test.js +56 -0
- package/bin/grill-model-docs.test.js +155 -0
- package/bin/grilling-holistic-review-docs.test.js +7 -3
- package/bin/lib/codex-projection.js +30 -14
- package/bin/lib/codex-projection.test.js +81 -17
- package/bin/lib/host-descriptor.js +15 -2
- package/bin/lib/host-descriptor.test.js +24 -0
- package/bin/lib/runtime-command-literals.test.js +184 -0
- package/bin/report-status-docs.test.js +11 -3
- package/bin/session-trust-docs.test.js +37 -0
- package/bin/surfaced-scope-docs.test.js +57 -0
- package/bin/trailhead.js +7 -5
- package/bin/trailhead.test.js +53 -11
- package/bin/unblock-docs.test.js +56 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/agents/trailhead-code-review.md +2 -2
- package/plugins/trailhead/agents/trailhead-codebase-map.md +1 -1
- package/plugins/trailhead/agents/trailhead-grill.md +41 -0
- package/plugins/trailhead/agents/trailhead-verify.md +2 -2
- package/plugins/trailhead/hooks/lib/capture.test.js +439 -0
- package/plugins/trailhead/hooks/lib/checkout-find.test.js +195 -0
- package/plugins/trailhead/hooks/lib/checkout-in.test.js +83 -7
- package/plugins/trailhead/hooks/lib/dashboard.test.js +19 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.js +89 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.test.js +132 -0
- package/plugins/trailhead/hooks/lib/inherit-session.js +17 -0
- package/plugins/trailhead/hooks/lib/issue-body.test.js +84 -0
- package/plugins/trailhead/hooks/lib/model-defaults-review.js +72 -22
- package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +113 -0
- package/plugins/trailhead/hooks/lib/model-route.js +207 -0
- package/plugins/trailhead/hooks/lib/model-route.test.js +231 -0
- package/plugins/trailhead/hooks/lib/report-status.test.js +124 -2
- package/plugins/trailhead/hooks/lib/shell-scan.js +25 -0
- package/plugins/trailhead/hooks/lib/statusline-context.test.js +22 -1
- package/plugins/trailhead/hooks/lib/unmodelled.js +6 -1
- package/plugins/trailhead/hooks/mod/README.md +24 -10
- package/plugins/trailhead/hooks/mod/map-pane.tsx +124 -44
- package/plugins/trailhead/hooks/mod/my-tickets.ts +76 -0
- package/plugins/trailhead/hooks/mod/repo-pin.ts +8 -5
- package/plugins/trailhead/hooks/trailhead-check-update.js +17 -1
- package/plugins/trailhead/hooks/trailhead-check-update.test.js +60 -0
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +40 -1
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +68 -0
- package/plugins/trailhead/skills/_shared/capture-core.md +118 -0
- package/plugins/trailhead/skills/_shared/choices.md +2 -2
- package/plugins/trailhead/skills/_shared/configuration-reference.md +20 -18
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/session-handoff.md +2 -2
- package/plugins/trailhead/skills/_shared/substrate-commands.md +8 -4
- package/plugins/trailhead/skills/_shared/substrate.md +1 -1
- package/plugins/trailhead/skills/_shared/surfaced-scope.md +23 -0
- package/plugins/trailhead/skills/_shared/teamwork.md +9 -5
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +5 -2
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/debug.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +26 -1
- package/plugins/trailhead/skills/_shared/techniques/research.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/verify.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques.md +7 -5
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +2 -4
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +29 -4
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +3 -3
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +5 -4
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +13 -11
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +1 -1
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +11 -10
- package/plugins/trailhead/templates/statusline-trailhead.sh +3 -3
- package/plugins/trailhead/templates/trailhead-capture.js +418 -0
- package/plugins/trailhead/templates/trailhead-checkout-find.js +256 -0
- package/plugins/trailhead/templates/trailhead-checkout-in.js +76 -20
- package/plugins/trailhead/templates/trailhead-dashboard.js +15 -2
- package/plugins/trailhead/templates/trailhead-issue-body.js +69 -6
- package/plugins/trailhead/templates/trailhead-report-status.js +123 -11
- package/plugins/trailhead/tests/map-pane-ui.test.ts +299 -37
- package/plugins/trailhead/tests/my-tickets.test.ts +66 -0
- package/plugins/trailhead/tests/repo-pin.test.ts +20 -5
- package/plugins/trailhead/types/index.d.ts +7 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Test for #334: the composite Action actions/capture is a thin wrapper over the capture
|
|
3
|
+
// helper. Checks the input/output mirror, injection safety, the run block against a stub
|
|
4
|
+
// helper, and the docs. Plain asserts, no framework, no YAML dependency.
|
|
5
|
+
// Run: node bin/capture-action.test.js
|
|
6
|
+
const assert = require('assert');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { spawnSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
const root = path.resolve(__dirname, '..');
|
|
13
|
+
const actionPath = path.join(root, 'actions', 'capture', 'action.yml');
|
|
14
|
+
let passed = 0;
|
|
15
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
16
|
+
const eq = (name, a, b) => { assert.deepStrictEqual(a, b, name); passed++; };
|
|
17
|
+
const EMDASH = String.fromCharCode(0x2014);
|
|
18
|
+
|
|
19
|
+
const yml = fs.readFileSync(actionPath, 'utf8');
|
|
20
|
+
const lines = yml.split('\n');
|
|
21
|
+
|
|
22
|
+
// Top-level keys under a section (2-space indented names) of the minimal YAML.
|
|
23
|
+
function sectionKeys(name) {
|
|
24
|
+
const i = lines.findIndex((l) => l === `${name}:`);
|
|
25
|
+
ok(`action.yml has a ${name}: section`, i >= 0);
|
|
26
|
+
const keys = [];
|
|
27
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
28
|
+
if (lines[j] !== '' && !lines[j].startsWith(' ')) break;
|
|
29
|
+
const m = /^ {2}([A-Za-z0-9_-]+):/.exec(lines[j]);
|
|
30
|
+
if (m) keys.push(m[1]);
|
|
31
|
+
}
|
|
32
|
+
return keys;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Mirror
|
|
36
|
+
eq('inputs mirror the helper flags plus token',
|
|
37
|
+
sectionKeys('inputs').sort(),
|
|
38
|
+
['body', 'body-file', 'dedupe-key', 'map', 'no-dashboard', 'of', 'repo', 'title', 'token', 'trigger', 'type', 'unverified']);
|
|
39
|
+
eq('outputs are issue, url, status', sectionKeys('outputs').sort(), ['issue', 'status', 'url']);
|
|
40
|
+
for (const name of ['issue', 'url', 'status']) {
|
|
41
|
+
ok(`output ${name} reads the capture step`, yml.includes(`value: \${{ steps.capture.outputs.${name} }}`));
|
|
42
|
+
}
|
|
43
|
+
ok('composite action', /^ {2}using: composite$/m.test(yml));
|
|
44
|
+
ok('the step has id: capture', /^ {4}- id: capture$/m.test(yml));
|
|
45
|
+
ok('description notes issues: write', /issues: write/.test(yml.split('\ninputs:')[0]));
|
|
46
|
+
ok('branding present', /^branding:/m.test(yml));
|
|
47
|
+
|
|
48
|
+
// Extract the literal run block (fixed indentation: `run: |` then 8-space lines).
|
|
49
|
+
const runIdx = lines.findIndex((l) => /^ {6}run: \|$/.test(l));
|
|
50
|
+
assert.ok(runIdx >= 0, 'run: | literal block not found in action.yml');
|
|
51
|
+
const runLines = [];
|
|
52
|
+
for (let j = runIdx + 1; j < lines.length; j++) {
|
|
53
|
+
if (lines[j] === '') { runLines.push(''); continue; }
|
|
54
|
+
if (!lines[j].startsWith(' ')) break;
|
|
55
|
+
runLines.push(lines[j].slice(8));
|
|
56
|
+
}
|
|
57
|
+
const script = runLines.join('\n');
|
|
58
|
+
ok('run block is non-empty', script.trim().length > 0);
|
|
59
|
+
|
|
60
|
+
// Injection safety
|
|
61
|
+
ok('no ${{ expression inside the run script', !script.includes('${{'));
|
|
62
|
+
ok('inputs reach the script through env:', /^ {6}env:$/m.test(yml) && /INPUT_BODY: \$\{\{ inputs\.body \}\}/.test(yml));
|
|
63
|
+
|
|
64
|
+
// Runtime against a stub helper
|
|
65
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'capture-action-'));
|
|
66
|
+
const actionDir = path.join(tmp, 'actions', 'capture');
|
|
67
|
+
const tplDir = path.join(tmp, 'plugins', 'trailhead', 'templates');
|
|
68
|
+
fs.mkdirSync(actionDir, { recursive: true });
|
|
69
|
+
fs.mkdirSync(tplDir, { recursive: true });
|
|
70
|
+
fs.writeFileSync(path.join(tmp, 'run.sh'), script);
|
|
71
|
+
fs.writeFileSync(path.join(tplDir, 'trailhead-capture.js'), `
|
|
72
|
+
const fs = require('fs');
|
|
73
|
+
fs.writeFileSync(process.env.STUB_ARGV_FILE, JSON.stringify({ argv: process.argv.slice(2), token: process.env.GH_TOKEN || null }));
|
|
74
|
+
if (process.env.STUB_RAW !== undefined) process.stdout.write(process.env.STUB_RAW);
|
|
75
|
+
else console.log(process.env.STUB_JSON || '{"status":"created","issue":7,"url":"https://github.com/o/r/issues/7"}');
|
|
76
|
+
process.exit(Number(process.env.STUB_EXIT || 0));
|
|
77
|
+
`);
|
|
78
|
+
|
|
79
|
+
const baseInputs = { INPUT_TYPE: 'bug', INPUT_TITLE: 'T', INPUT_REPO: 'o/r', INPUT_TOKEN: 'tok' };
|
|
80
|
+
function run(inputs, stub = {}) {
|
|
81
|
+
const out = path.join(tmp, 'output.txt');
|
|
82
|
+
const argvFile = path.join(tmp, 'argv.json');
|
|
83
|
+
fs.rmSync(out, { force: true });
|
|
84
|
+
fs.rmSync(argvFile, { force: true });
|
|
85
|
+
const env = {
|
|
86
|
+
PATH: process.env.PATH,
|
|
87
|
+
GITHUB_ACTION_PATH: actionDir,
|
|
88
|
+
GITHUB_OUTPUT: out,
|
|
89
|
+
STUB_ARGV_FILE: argvFile,
|
|
90
|
+
GH_TOKEN: inputs.INPUT_TOKEN || '',
|
|
91
|
+
...inputs,
|
|
92
|
+
...stub,
|
|
93
|
+
};
|
|
94
|
+
const r = spawnSync('bash', ['--noprofile', '--norc', '-eo', 'pipefail', path.join(tmp, 'run.sh')], { env, encoding: 'utf8' });
|
|
95
|
+
const outputs = {};
|
|
96
|
+
if (fs.existsSync(out)) {
|
|
97
|
+
for (const l of fs.readFileSync(out, 'utf8').split('\n')) {
|
|
98
|
+
const m = /^([a-z]+)=(.*)$/.exec(l);
|
|
99
|
+
if (m) outputs[m[1]] = m[2];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const seen = fs.existsSync(argvFile) ? JSON.parse(fs.readFileSync(argvFile, 'utf8')) : null;
|
|
103
|
+
return { status: r.status, outputs, seen, stderr: r.stderr };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let r = run(baseInputs);
|
|
107
|
+
eq('minimal argv: empty optionals omitted', r.seen.argv, ['--type', 'bug', '--title', 'T', '--repo', 'o/r']);
|
|
108
|
+
eq('exit 0 on created', r.status, 0);
|
|
109
|
+
eq('outputs from the JSON line', r.outputs, { issue: '7', url: 'https://github.com/o/r/issues/7', status: 'created' });
|
|
110
|
+
eq('GH_TOKEN reaches the helper', r.seen.token, 'tok');
|
|
111
|
+
|
|
112
|
+
r = run({ ...baseInputs, INPUT_BODY: 'a\nb "c" $HOME\n', INPUT_BODY_FILE: 'f.md', INPUT_MAP: '5', INPUT_OF: '9', INPUT_TRIGGER: 'x y',
|
|
113
|
+
INPUT_UNVERIFIED: 'true', INPUT_DEDUPE_KEY: 'k1', INPUT_NO_DASHBOARD: 'true' });
|
|
114
|
+
eq('full argv maps 1:1, multi-line body intact, booleans bare',
|
|
115
|
+
r.seen.argv,
|
|
116
|
+
['--type', 'bug', '--title', 'T', '--body', 'a\nb "c" $HOME\n', '--body-file', 'f.md', '--repo', 'o/r', '--map', '5', '--of', '9',
|
|
117
|
+
'--trigger', 'x y', '--unverified', '--dedupe-key', 'k1', '--no-dashboard']);
|
|
118
|
+
|
|
119
|
+
r = run({ ...baseInputs, INPUT_UNVERIFIED: 'false', INPUT_NO_DASHBOARD: '' });
|
|
120
|
+
ok('false/empty booleans are absent', !r.seen.argv.includes('--unverified') && !r.seen.argv.includes('--no-dashboard'));
|
|
121
|
+
|
|
122
|
+
r = run(baseInputs, { STUB_JSON: '{"status":"existing"}' });
|
|
123
|
+
eq('status only when JSON lacks issue/url', r.outputs, { issue: '', url: '', status: 'existing' });
|
|
124
|
+
eq('existing exits 0', r.status, 0);
|
|
125
|
+
|
|
126
|
+
for (const code of [3, 1, 2]) {
|
|
127
|
+
r = run(baseInputs, { STUB_EXIT: String(code), STUB_JSON: `{"status":"x${code}","issue":4,"url":"u"}` });
|
|
128
|
+
eq(`helper exit ${code} propagates`, r.status, code);
|
|
129
|
+
eq(`status still written on exit ${code}`, r.outputs.status, `x${code}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const [label, raw] of [['malformed', 'not json\n'], ['empty', '']]) {
|
|
133
|
+
r = run(baseInputs, { STUB_RAW: raw, STUB_EXIT: '3' });
|
|
134
|
+
eq(`${label} stdout never masks exit 3`, r.status, 3);
|
|
135
|
+
eq(`${label} stdout leaves outputs empty`, { issue: r.outputs.issue || '', url: r.outputs.url || '', status: r.outputs.status || '' }, { issue: '', url: '', status: '' });
|
|
136
|
+
r = run(baseInputs, { STUB_RAW: raw, STUB_EXIT: '0' });
|
|
137
|
+
eq(`${label} stdout with exit 0 exits 1`, r.status, 1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
141
|
+
|
|
142
|
+
// Docs
|
|
143
|
+
const readme = fs.readFileSync(path.join(root, 'README.md'), 'utf8');
|
|
144
|
+
const readmeIt = fs.readFileSync(path.join(root, 'README.it.md'), 'utf8');
|
|
145
|
+
for (const [name, doc] of [['README.md', readme], ['README.it.md', readmeIt]]) {
|
|
146
|
+
ok(`${name} names the Action`, doc.includes('ToRvaLDz/trailhead/actions/capture@'));
|
|
147
|
+
ok(`${name} names issues: write`, doc.includes('issues: write'));
|
|
148
|
+
ok(`${name} names dedupe-key`, doc.includes('dedupe-key'));
|
|
149
|
+
}
|
|
150
|
+
const capDoc = fs.readFileSync(path.join(root, 'plugins', 'trailhead', 'skills', 'trailhead-capture', 'references', 'capture.md'), 'utf8');
|
|
151
|
+
ok('capture.md names the Action', capDoc.includes('actions/capture'));
|
|
152
|
+
for (const [name, text] of [['action.yml', yml], ['README.md', readme], ['README.it.md', readmeIt], ['capture.md', capDoc], ['test', fs.readFileSync(__filename, 'utf8').replace(/String\.fromCharCode\(0x2014\)/g, '')]]) {
|
|
153
|
+
ok(`no em-dash in ${name}`, !text.includes(EMDASH));
|
|
154
|
+
}
|
|
155
|
+
ok('real helper path used by the run block exists',
|
|
156
|
+
fs.existsSync(path.join(root, 'actions/capture', '../../plugins/trailhead/templates/trailhead-capture.js')));
|
|
157
|
+
for (const [name, doc] of [['README.md', readme], ['README.it.md', readmeIt]]) {
|
|
158
|
+
const keys = [...doc.matchAll(/^\s*dedupe-key: (.+)$/gm)].map((m) => m[1]);
|
|
159
|
+
ok(`${name} has a dedupe-key example`, keys.length > 0);
|
|
160
|
+
for (const k of keys) {
|
|
161
|
+
ok(`${name} dedupe-key example avoids unsafe contexts`, !/github\.(ref_name|workflow|head_ref|ref)\b/.test(k));
|
|
162
|
+
// job ids are [A-Za-z0-9_-]; run_id and run_number are digits
|
|
163
|
+
const resolved = k.replace(/\$\{\{\s*github\.(job|run_id|run_number)\s*\}\}/g, 'x1');
|
|
164
|
+
ok(`${name} dedupe-key example matches the helper regex`, /^[A-Za-z0-9._:-]{1,100}$/.test(resolved));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
console.log(`capture-action.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test for #332: capture.md documents the scripted capture helper (CLI, statuses,
|
|
3
|
+
// exit codes, trust, idea as fog, dedupe, dashboard). Plain asserts, no framework.
|
|
4
|
+
// Run: node bin/capture-docs.test.js
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(__dirname, '..');
|
|
10
|
+
const doc = fs.readFileSync(path.join(root, 'plugins', 'trailhead', 'skills', 'trailhead-capture', 'references', 'capture.md'), 'utf8');
|
|
11
|
+
const start = doc.indexOf('## Scripted capture for automations');
|
|
12
|
+
const section = start < 0 ? '' : doc.slice(start);
|
|
13
|
+
let passed = 0;
|
|
14
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
15
|
+
|
|
16
|
+
ok('capture.md has the scripted capture section', section !== '');
|
|
17
|
+
ok('names the helper', section.includes('trailhead-capture.js'));
|
|
18
|
+
for (const flag of ['--type', '--title', '--body-file', '--repo', '--map', '--of', '--trigger', '--unverified', '--dedupe-key', '--no-dashboard']) {
|
|
19
|
+
ok(`names ${flag}`, section.includes(flag));
|
|
20
|
+
}
|
|
21
|
+
for (const status of ['created', 'existing', 'partial', 'error', 'usage']) {
|
|
22
|
+
ok(`documents status ${status}`, section.includes(`"status":"${status}"`));
|
|
23
|
+
}
|
|
24
|
+
ok('documents exit codes 0, 1, 2 and 3', ['exit 0', 'exit 1', 'exit 2', 'exit 3'].every((c) => section.includes(c)));
|
|
25
|
+
ok('partial names prune', /partial[^]*\/trailhead:prune/.test(section));
|
|
26
|
+
ok('trust: allowlist is the repo variable, not the local env', section.includes('TRAILHEAD_LABEL_ALLOWLIST') && /never consults its local environment/.test(section));
|
|
27
|
+
ok('trust: installation tokens are verified post-create', section.includes('github-actions[bot]'));
|
|
28
|
+
ok('idea is parked fog', /`idea` is parked fog/.test(section) && section.includes('`trailhead:fog`'));
|
|
29
|
+
ok('dedupe: marker, cap fallback and the lag residual', section.includes('trailhead-dedupe') && section.includes('1000-result cap') && /search-index lag/.test(section));
|
|
30
|
+
ok('dashboard rule: whiteboard only, update-only', /whiteboard ticket refreshes the dashboard/.test(section) && section.includes('--update-only'));
|
|
31
|
+
ok('no em-dash', !doc.includes(String.fromCharCode(0x2014)));
|
|
32
|
+
console.log(`capture-docs.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test: il lookup di <templates> negli skill rispetta CLAUDE_CONFIG_DIR (npm install),
|
|
3
|
+
// quindi nessun percorso nudo ~/.claude/trailhead/templates; il candidato Codex resta.
|
|
4
|
+
// Plain asserts, no framework. Run: node bin/config-dir-templates-docs.test.js
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(__dirname, '..');
|
|
10
|
+
const SKILLS = path.join(root, 'plugins/trailhead/skills');
|
|
11
|
+
let passed = 0;
|
|
12
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
13
|
+
|
|
14
|
+
const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
|
|
15
|
+
e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)]);
|
|
16
|
+
// Radici doc che il motore legge: skill, agent e comandi
|
|
17
|
+
const DOC_ROOTS = ['skills', 'agents', 'commands'].map((d) => path.join(root, 'plugins/trailhead', d));
|
|
18
|
+
const files = DOC_ROOTS.flatMap(walk).filter((f) => f.endsWith('.md'));
|
|
19
|
+
|
|
20
|
+
// Ogni grafia nuda della home: ~ o $HOME (la forma buona ha `}` dopo .claude, quindi non combacia)
|
|
21
|
+
const BARE = /(?:~|\$HOME)\/\.claude\/trailhead\/templates/g;
|
|
22
|
+
const GOOD = '${CLAUDE_CONFIG_DIR:-~/.claude}/trailhead/templates/';
|
|
23
|
+
const CODEX = '~/.codex/skills/trailhead/templates/';
|
|
24
|
+
|
|
25
|
+
for (const f of files) {
|
|
26
|
+
const rel = path.relative(root, f);
|
|
27
|
+
ok(`${rel}: no bare ~/.claude/trailhead/templates`, !(fs.readFileSync(f, 'utf8').match(BARE)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Siti noti: candidato Claude con CLAUDE_CONFIG_DIR, candidato Codex intatto
|
|
31
|
+
for (const rel of [
|
|
32
|
+
'trailhead-work/SKILL.md',
|
|
33
|
+
'_shared/techniques.md',
|
|
34
|
+
'_shared/teamwork.md',
|
|
35
|
+
'trailhead-manage/references/auditing.md',
|
|
36
|
+
]) {
|
|
37
|
+
const t = fs.readFileSync(path.join(SKILLS, rel), 'utf8');
|
|
38
|
+
ok(`${rel}: config-dir candidate present`, t.includes(GOOD));
|
|
39
|
+
ok(`${rel}: codex candidate untouched`, t.includes(CODEX));
|
|
40
|
+
}
|
|
41
|
+
const cfg = fs.readFileSync(path.join(SKILLS, '_shared/configuration-reference.md'), 'utf8');
|
|
42
|
+
ok('configuration-reference: statusline lookup uses config dir', cfg.includes(GOOD + 'statusline-trailhead.sh'));
|
|
43
|
+
|
|
44
|
+
// #319: l'install della statusline va nella config dir, non in ~/.claude nudo
|
|
45
|
+
const stl = cfg.split('\n').find((l) => l.includes('On **Enable**, install it')) || '';
|
|
46
|
+
ok('configuration-reference: statusline install line found', stl.length > 0);
|
|
47
|
+
ok('configuration-reference: statusline target and settings use config dir',
|
|
48
|
+
stl.includes('${CLAUDE_CONFIG_DIR:-~/.claude}/statusline-trailhead.sh') && stl.includes('${CLAUDE_CONFIG_DIR:-~/.claude}/settings.json'));
|
|
49
|
+
ok('configuration-reference: statusline install has no bare ~/.claude target or $HOME command',
|
|
50
|
+
!/(?<!:-)~\/\.claude\/(settings\.json|statusline-trailhead\.sh)/.test(stl) && !stl.includes('$HOME/.claude'));
|
|
51
|
+
|
|
52
|
+
console.log(`config-dir-templates-docs.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test: una decision chiusa porta il suo lavoro nei build (riga `Builds:`), il grill
|
|
3
|
+
// che risolve un ticket fa lo stesso, e l'audit segnala le decisioni chiuse senza build.
|
|
4
|
+
// Plain asserts, no framework. Run: node bin/decision-builds-docs.test.js
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(__dirname, '..');
|
|
10
|
+
const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
|
|
11
|
+
let passed = 0;
|
|
12
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
13
|
+
const EM = String.fromCharCode(0x2014);
|
|
14
|
+
|
|
15
|
+
// Sezione dal suo heading fino al prossimo heading dello stesso livello (o superiore).
|
|
16
|
+
const section = (text, headingRe) => {
|
|
17
|
+
const m = headingRe.exec(text);
|
|
18
|
+
if (!m) return '';
|
|
19
|
+
const level = m[0].match(/^#+/)[0].length;
|
|
20
|
+
const rest = text.slice(m.index + m[0].length);
|
|
21
|
+
const next = new RegExp(`^#{1,${level}} `, 'm').exec(rest);
|
|
22
|
+
return next ? rest.slice(0, next.index) : rest;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const SK = 'plugins/trailhead/skills';
|
|
26
|
+
|
|
27
|
+
// Motore decision
|
|
28
|
+
const engines = read(`${SK}/trailhead-work/references/ticket-engines.md`);
|
|
29
|
+
const dec = section(engines, /^## `decision`.*$/m);
|
|
30
|
+
ok('decision engine: section found', dec.length > 0);
|
|
31
|
+
ok('decision engine: a build follow-through step', /Carry it into build/.test(dec));
|
|
32
|
+
ok('decision engine: records a `Builds:` line', dec.includes('`Builds:'));
|
|
33
|
+
ok('decision engine: allows `Builds: none` only with a reason', /`Builds: none \(<why>\)`/.test(dec));
|
|
34
|
+
ok('decision engine: links existing tickets or creates build tickets', /existing/i.test(dec) && /create-then-wire/.test(dec));
|
|
35
|
+
ok('decision engine: the human confirms the build split in the final confirmation', /final confirmation/i.test(dec));
|
|
36
|
+
ok('decision engine: no close without the line', /never `gh issue close`/i.test(dec));
|
|
37
|
+
ok('decision engine: explains the fog that vanishes', /Not yet specified/.test(dec));
|
|
38
|
+
ok('decision engine: no em-dash', !dec.includes(EM));
|
|
39
|
+
|
|
40
|
+
// Work the map
|
|
41
|
+
const work = read(`${SK}/trailhead-work/SKILL.md`);
|
|
42
|
+
const step5 = (work.match(/^5\. Record the resolution:.*$/m) || [''])[0];
|
|
43
|
+
ok('work step 5: decision close waits on the `Builds:` line', step5.includes('`Builds:') && /Carry it into build/.test(step5));
|
|
44
|
+
|
|
45
|
+
// Grill su un ticket
|
|
46
|
+
const chart = read(`${SK}/trailhead-chart/SKILL.md`);
|
|
47
|
+
const grill = (chart.match(/^Run a standalone \*\*Grilling\*\*.*$/m) || [''])[0];
|
|
48
|
+
ok('grill: resolving a decision ticket carries it into build', /Carry it into build/.test(grill) && grill.includes('`Builds:'));
|
|
49
|
+
|
|
50
|
+
// Audit
|
|
51
|
+
const audit = read(`${SK}/trailhead-manage/references/auditing.md`);
|
|
52
|
+
const light = (audit.match(/^\*\*Light check\*\*.*$/m) || [''])[0];
|
|
53
|
+
ok('audit light check: flags a closed decision with no build', /no build/i.test(light) && light.includes('`Builds:'));
|
|
54
|
+
ok('audit light check: an older decision counts a build that links it', /links? (it|back)/i.test(light));
|
|
55
|
+
const report = section(audit, /^## 8\. Report$/m);
|
|
56
|
+
ok('audit report: lists decisions with no build', /no build/i.test(report));
|
|
57
|
+
const props = section(audit, /^## 9\. Proposals.*$/m);
|
|
58
|
+
ok('audit proposals: open the missing build tickets on a yes', /No build/.test(props) && /build/.test(props) && /`Builds:`/.test(props));
|
|
59
|
+
|
|
60
|
+
// Docs utente
|
|
61
|
+
const readme = read('README.md');
|
|
62
|
+
ok('README: decision engine leaves build tickets', /`decision` diverges options then grills to choose, then opens the build tickets/.test(readme));
|
|
63
|
+
ok('README: audit names decisions with no build', /decisions closed with no build/i.test(readme));
|
|
64
|
+
const readmeIt = read('README.it.md');
|
|
65
|
+
ok('README.it: decision engine leaves build tickets', /`decision` dirama le opzioni poi fa grilling per scegliere, poi apre i ticket build/.test(readmeIt));
|
|
66
|
+
ok('README.it: audit names decisions with no build', /decisioni chiuse senza build/i.test(readmeIt));
|
|
67
|
+
const wf = read('site/src/content/docs/docs/workflow.md');
|
|
68
|
+
ok('workflow.md: decision engine leaves build tickets', /`decision` diverges options then grills to choose, then opens the build tickets/.test(wf));
|
|
69
|
+
const manage = read('site/src/content/docs/docs/commands/manage.md');
|
|
70
|
+
ok('manage.md: audit names decisions with no build', /closed with no build/i.test(manage));
|
|
71
|
+
for (const [n, t] of [['README.md', readme], ['README.it.md', readmeIt], ['workflow.md', wf], ['manage.md', manage]]) {
|
|
72
|
+
ok(`${n}: no em-dash`, !t.includes(EM));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(`decision-builds-docs.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test per #325: ogni step del motore decide inline vs dispatch con
|
|
3
|
+
// model-route.js, non con la regola in prosa. Il testo canonico vive una volta
|
|
4
|
+
// in techniques.md; ogni punto di dispatch ha un gate imperativo che lo richiama.
|
|
5
|
+
// Run: node engine-route-docs.test.js
|
|
6
|
+
const assert = require('assert');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const root = path.join(__dirname, '..');
|
|
11
|
+
const skills = path.join(root, 'plugins', 'trailhead', 'skills');
|
|
12
|
+
const read = (rel) => fs.readFileSync(path.join(skills, rel), 'utf8');
|
|
13
|
+
|
|
14
|
+
let passed = 0;
|
|
15
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
16
|
+
|
|
17
|
+
const SECTION = '**Route every step through model-route.js.**';
|
|
18
|
+
const SECTION_NAME = 'Route every step through model-route.js';
|
|
19
|
+
|
|
20
|
+
// --- Parte A: la sezione canonica in techniques.md -----------------------------------
|
|
21
|
+
{
|
|
22
|
+
const techniques = read('_shared/techniques.md');
|
|
23
|
+
const at = techniques.indexOf(SECTION);
|
|
24
|
+
ok('A: techniques.md has the canonical routing paragraph', at !== -1);
|
|
25
|
+
const sec = at === -1 ? '' : techniques.slice(at, techniques.indexOf('\n\n', at));
|
|
26
|
+
const has = (...needles) => needles.every((n) => sec.includes(n));
|
|
27
|
+
ok('A: the command with every flag',
|
|
28
|
+
has('model-route.js <key> --session <your full model id>', '--session-effort', '--host codex', '--no-subagents', '<project root>'));
|
|
29
|
+
ok('A: the two-path <hooks> lookup', has('../../hooks/lib/', '../trailhead/hooks/lib/', 'model-defaults-review.js'));
|
|
30
|
+
ok('A: where the model id comes from', has('The exact model ID is', 'pass `unknown` only if it is truly absent'));
|
|
31
|
+
ok('A: the key per step', has(
|
|
32
|
+
'Plan -> `plan`', 'Execute -> `execute`', 'Fix -> `execute`', 'Code review -> `review`',
|
|
33
|
+
'Goal-backward verification -> `verify`', 'Research -> `research`', 'Debug -> `debug`',
|
|
34
|
+
'Codebase map -> `codebase-map`', 'Grilling -> `grill`'));
|
|
35
|
+
ok('A: Fix spawns trailhead-fix in place of the returned agent', has('spawn `trailhead-fix` in place of the returned `agent`'));
|
|
36
|
+
ok('A: Grilling keeps its own section', has('**Who runs a round**'));
|
|
37
|
+
ok('A: route inline, suggest and warning', has('`route: inline`', '`suggest`', '`warning`'));
|
|
38
|
+
ok('A: route dispatch uses agent and alias verbatim', has('`route: dispatch`', 'use its `agent` and `alias` verbatim'));
|
|
39
|
+
ok('A: no hand decision', has('Never decide inline versus dispatch by hand', 'never read the config yourself'));
|
|
40
|
+
ok('A: an unregistered agent runs inline', has('not registered', 'inline'));
|
|
41
|
+
ok('A: the lean exception', has('accepted `lean` skip'));
|
|
42
|
+
ok('A: the autonomous-run exception dispatches with no model override on an inline route',
|
|
43
|
+
has('more than one slot', 'no model override'));
|
|
44
|
+
ok('A: Codex spawns by agent_type, with a null alias', has('`agent_type=<agent>` on Codex', '`alias` is null'));
|
|
45
|
+
ok('A: Claude Code passes alias as the Agent model override', has('`alias` as the `Agent` model override on Claude Code'));
|
|
46
|
+
const rule = sec.indexOf('Never decide inline versus dispatch by hand');
|
|
47
|
+
const lean = sec.indexOf('accepted `lean` skip');
|
|
48
|
+
const auto = sec.indexOf('more than one slot');
|
|
49
|
+
ok('A: both exclusions are stated after the rule, inside the paragraph',
|
|
50
|
+
rule !== -1 && lean > rule && auto > rule && sec.indexOf('Two cases sit outside the route') > rule);
|
|
51
|
+
ok('A: the helper still runs in the autonomous case', sec.slice(auto).includes('the helper is still run'));
|
|
52
|
+
const dispatch = techniques.split('\n')[4] || '';
|
|
53
|
+
ok('A: the dispatch paragraph points at the routing paragraph', dispatch.includes(SECTION_NAME));
|
|
54
|
+
ok('A: the dispatch paragraph no longer restates the hand rule',
|
|
55
|
+
!dispatch.includes('running inline instead only when') && !dispatch.includes('Keep the inline-vs-subagent decision on the full id'));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- Parte B: il gate a ogni punto di dispatch ---------------------------------------
|
|
59
|
+
const gateOk = (text, key) =>
|
|
60
|
+
text.includes(`model-route.js ${key} --session <your full model id> <project root>`) &&
|
|
61
|
+
text.includes('obey its `route`') &&
|
|
62
|
+
text.includes('never decide inline versus dispatch by hand') &&
|
|
63
|
+
text.includes(SECTION_NAME);
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
const engines = read('trailhead-work/references/ticket-engines.md');
|
|
67
|
+
const lines = engines.split('\n');
|
|
68
|
+
const line = (prefix) => lines.find((l) => l.startsWith(prefix)) || '';
|
|
69
|
+
ok('B: build Plan gates on plan', gateOk(line('2. **Plan**:'), 'plan'));
|
|
70
|
+
ok('B: build Execute Dispatch gates on execute', gateOk(line(' - **Dispatch.** Spawn the **`trailhead-executor`**'), 'execute'));
|
|
71
|
+
ok('B: bug Diagnose gates on debug', gateOk(line('2. **Diagnose**:'), 'debug'));
|
|
72
|
+
const fix = line(' - **Dispatch.** Spawn the **`trailhead-fix`**');
|
|
73
|
+
ok('B: bug Fix Dispatch gates on execute and spawns trailhead-fix', gateOk(fix, 'execute') && fix.includes('`trailhead-fix`'));
|
|
74
|
+
ok('B: ticket-engines.md no longer restates the hand rule', !engines.includes('Inline only when the resolved model'));
|
|
75
|
+
const GATE = '**First, route it.**';
|
|
76
|
+
for (const [name, l] of [['Plan', line('2. **Plan**:')], ['Execute', line(' - **Dispatch.** Spawn the **`trailhead-executor`**')],
|
|
77
|
+
['Diagnose', line('2. **Diagnose**:')], ['Fix', fix]]) {
|
|
78
|
+
ok(`B: ${name} spawns by agent_type on Codex`, l.includes('agent_type='));
|
|
79
|
+
const g = l.indexOf(GATE);
|
|
80
|
+
ok(`B: ${name} gate comes before any lean-skip or exception wording`, g !== -1 && (l.indexOf('`lean`') === -1 || g < l.indexOf('`lean`')));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const [file, key, agent] of [
|
|
85
|
+
['code-review.md', 'review', 'trailhead-code-review'],
|
|
86
|
+
['verify.md', 'verify', 'trailhead-verify'],
|
|
87
|
+
['research.md', 'research', 'trailhead-research'],
|
|
88
|
+
['debug.md', 'debug', 'trailhead-debug'],
|
|
89
|
+
['codebase-map.md', 'codebase-map', 'trailhead-codebase-map'],
|
|
90
|
+
]) {
|
|
91
|
+
const text = read(`_shared/techniques/${file}`);
|
|
92
|
+
const gateAt = text.indexOf('**First, route it.**');
|
|
93
|
+
const dispatchAt = text.indexOf(`**\`${agent}\`** agent`);
|
|
94
|
+
const gate = gateAt === -1 ? '' : text.slice(gateAt, text.indexOf('\n', gateAt));
|
|
95
|
+
ok(`B: ${file} has a routing gate before its dispatch sentence`, gateAt !== -1 && dispatchAt !== -1 && gateAt < dispatchAt);
|
|
96
|
+
ok(`B: ${file} gate runs model-route.js ${key}`, gateOk(gate, key));
|
|
97
|
+
ok(`B: ${file} gate exempts the dispatched ${agent}`, gate.includes(`If you are the \`${agent}\` agent, skip this gate`));
|
|
98
|
+
ok(`B: ${file} no longer restates the hand rule`, !text.includes('inline only when the resolved model'));
|
|
99
|
+
ok(`B: ${file} gate spawns by agent_type on Codex`, gate.includes('agent_type=<agent>'));
|
|
100
|
+
ok(`B: ${file} gate points at the canonical paragraph`, gate.includes('Route every step through model-route.js'));
|
|
101
|
+
ok(`B: ${file} gate comes before any lean-skip wording`, text.indexOf('`lean`') === -1 || gateAt < text.indexOf('`lean`'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- Parte B2: auto.md ---------------------------------------------------------------
|
|
105
|
+
{
|
|
106
|
+
const auto = read('trailhead-work/references/auto.md');
|
|
107
|
+
const line = auto.split('\n').find((l) => l.startsWith('- **Background dispatch, never inline.**')) || '';
|
|
108
|
+
ok('B: auto.md still dispatches every step in the background', line.includes('background subagent'));
|
|
109
|
+
ok('B: auto.md runs the helper per step', line.includes('model-route.js <key> --session <your full model id> <project root>') && line.includes('Route every step through model-route.js'));
|
|
110
|
+
ok('B: auto.md spawns with no model override on an inline route', line.includes('`route: inline`') && line.includes('no model override'));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- Parte B3: rifiniture della review di #325 --------------------------------------
|
|
114
|
+
{
|
|
115
|
+
const cr = read('_shared/techniques/code-review.md');
|
|
116
|
+
ok('B: code-review.md no longer says a lean skip is the only inline review', !cr.includes('only inline review'));
|
|
117
|
+
ok('B: code-review.md says the review runs inline on route: inline', cr.includes('in the session on `route: inline`'));
|
|
118
|
+
const audit = read('trailhead-manage/references/auditing.md');
|
|
119
|
+
ok('B: auditing.md routes verify once per run through model-route.js',
|
|
120
|
+
audit.includes('routes `verify` once per run through `model-route.js`') && audit.includes('Route every step through model-route.js'));
|
|
121
|
+
const agent = fs.readFileSync(path.join(root, 'plugins', 'trailhead', 'agents', 'trailhead-codebase-map.md'), 'utf8');
|
|
122
|
+
ok('B: the codebase-map agent no longer claims it has no model key', !agent.includes('no dedicated model key') && agent.includes('config.models.codebase-map'));
|
|
123
|
+
const cfg = read('_shared/configuration.md');
|
|
124
|
+
const ref = read('_shared/configuration-reference.md');
|
|
125
|
+
ok('B: configuration docs say every key routes through model-route.js',
|
|
126
|
+
!cfg.includes('every model key runs as a subagent') && !ref.includes('Every model key runs as a subagent') &&
|
|
127
|
+
cfg.includes('every model key routes through `model-route.js`') && ref.includes('Every model key routes through `model-route.js`'));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- Parte C: la config descrive l'helper, non una decisione a mano -------------------
|
|
131
|
+
{
|
|
132
|
+
const cfg = read('_shared/configuration.md');
|
|
133
|
+
const ref = read('_shared/configuration-reference.md');
|
|
134
|
+
ok('C: configuration.md lets the helper decide', cfg.includes('model-route.js') && !cfg.includes('Run Execute inline only when'));
|
|
135
|
+
const bullet = (k) => ref.split('\n').find((l) => l.startsWith(`- \`${k}\` →`)) || '';
|
|
136
|
+
ok('C: configuration-reference.md plan bullet points at the helper', bullet('plan').includes('model-route.js') && !bullet('plan').includes('inline only when'));
|
|
137
|
+
ok('C: configuration-reference.md execute bullet points at the helper', bullet('execute').includes('model-route.js') && !bullet('execute').includes('inline in the session instead when'));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- Parte D: l'helper copre ogni chiave nominata ------------------------------------
|
|
141
|
+
{
|
|
142
|
+
const { AGENT_OF } = require(path.join(root, 'plugins', 'trailhead', 'hooks', 'lib', 'model-route.js'));
|
|
143
|
+
for (const key of ['plan', 'execute', 'review', 'verify', 'research', 'debug', 'codebase-map', 'grill']) {
|
|
144
|
+
ok(`D: model-route.js knows ${key} on both hosts`, !!AGENT_OF.claude[key] && !!AGENT_OF.codex[key]);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log(`engine-route-docs.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test: il charting apre come ticket (bloccati se serve) le aree con una Question
|
|
3
|
+
// già scrivibile, lascia in fog solo il resto; dopo ogni risoluzione la fog viene
|
|
4
|
+
// riesaminata tutta; a frontier vuota si propone un gruppo di voci da graduare.
|
|
5
|
+
// Plain asserts, no framework. Run: node bin/fog-vs-ticket-docs.test.js
|
|
6
|
+
const assert = require('assert');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const root = path.resolve(__dirname, '..');
|
|
11
|
+
const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
|
|
12
|
+
let passed = 0;
|
|
13
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
14
|
+
const EM = String.fromCharCode(0x2014);
|
|
15
|
+
const line = (text, re) => (text.match(re) || [''])[0];
|
|
16
|
+
|
|
17
|
+
const SK = 'plugins/trailhead/skills';
|
|
18
|
+
|
|
19
|
+
// Charting: il criterio ticket vs fog
|
|
20
|
+
const charting = read(`${SK}/trailhead-chart/references/charting.md`);
|
|
21
|
+
const step4 = line(charting, /^4\. \*\*Create the tickets you can specify now\*\*.*$/m);
|
|
22
|
+
ok('charting step 4: names the ticket-or-fog test', /Ticket or fog/.test(step4));
|
|
23
|
+
ok('charting step 4: a writable Question makes a ticket even when blocked', /Question/.test(step4) && /blocked/.test(step4) && /not a reason to leave it in the fog/i.test(step4));
|
|
24
|
+
ok('charting step 4: fog only when the question cannot be phrased yet', /cannot phrase/i.test(step4));
|
|
25
|
+
ok('charting step 4: fog when an open decision could reshape the question, and the entry says which', /reshape/i.test(step4) && /names? (that|the) (decision|ticket)/i.test(step4));
|
|
26
|
+
ok('charting step 4: the 100 sub-issue cap is the only volume brake', /100/.test(step4));
|
|
27
|
+
ok('charting: no em-dash', !charting.includes(EM));
|
|
28
|
+
|
|
29
|
+
// Work the map: riesame di tutta la fog dopo ogni risoluzione
|
|
30
|
+
const work = read(`${SK}/trailhead-work/SKILL.md`);
|
|
31
|
+
const step6 = line(work, /^6\. Add newly-surfaced tickets.*$/m);
|
|
32
|
+
ok('work step 6: re-scans every fog entry', /every entry/i.test(step6) && /Not yet specified/.test(step6));
|
|
33
|
+
ok('work step 6: applies the charting test', /Ticket or fog/.test(step6));
|
|
34
|
+
ok('work step 6: proposes the graduations as one batch', /one batch/i.test(step6));
|
|
35
|
+
|
|
36
|
+
// Handoff a frontier vuota: un gruppo, non una voce
|
|
37
|
+
const handoff = read(`${SK}/_shared/session-handoff.md`);
|
|
38
|
+
const empty = line(handoff, /^\s*\*\*If the frontier is empty\*\*.*$/m);
|
|
39
|
+
ok('handoff: empty frontier proposes a batch of ripe fog entries', /batch/i.test(empty) && /Ticket or fog/.test(empty));
|
|
40
|
+
ok('handoff: no em-dash', !handoff.includes(EM));
|
|
41
|
+
|
|
42
|
+
// Docs utente
|
|
43
|
+
const docs = [
|
|
44
|
+
['README.md', read('README.md'), false],
|
|
45
|
+
['README.it.md', read('README.it.md'), true],
|
|
46
|
+
['concepts.md', read('site/src/content/docs/docs/concepts.md'), false],
|
|
47
|
+
];
|
|
48
|
+
for (const [n, t, it] of docs) {
|
|
49
|
+
ok(`${n}: fog no longer clears "one at a time"`, it ? !/una alla volta, finché non resta/.test(t) : !/ticket, one at a time, until nothing/.test(t));
|
|
50
|
+
ok(`${n}: a phrasable question is a ticket, blocked if needed`, it
|
|
51
|
+
? /domanda già formulabile[^.]*ticket[^.]*bloccat/i.test(t)
|
|
52
|
+
: /question you can already phrase[^.]*ticket[^.]*blocked/i.test(t));
|
|
53
|
+
ok(`${n}: no em-dash`, !t.includes(EM));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log(`fog-vs-ticket-docs.test.js: ${passed} checks passed`);
|