@hone-ai/cli 1.4.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/bin/hone.js +2 -0
- package/hone-cli.js +4006 -0
- package/lib/README.md +119 -0
- package/lib/adversarial-negative-lint.js +149 -0
- package/lib/audit.js +156 -0
- package/lib/auto-detect.js +213 -0
- package/lib/autofix-guardrails.js +124 -0
- package/lib/branch-protection.js +256 -0
- package/lib/ci-classifier.js +150 -0
- package/lib/ci-failures.js +173 -0
- package/lib/claude-md-tokens.js +71 -0
- package/lib/compliance-check.js +62 -0
- package/lib/config-augment.js +133 -0
- package/lib/config-update.js +70 -0
- package/lib/dependency-audit.js +108 -0
- package/lib/derive-domain.js +185 -0
- package/lib/doc-registry.js +63 -0
- package/lib/doctor-admin-merge.js +185 -0
- package/lib/doctor-bind-default.js +118 -0
- package/lib/doctor-docs.js +205 -0
- package/lib/doctor-placeholders.js +144 -0
- package/lib/doctor-skill-staleness.js +122 -0
- package/lib/domain-skill-template.md +114 -0
- package/lib/editor-detect.js +169 -0
- package/lib/fast-track-ratify.js +133 -0
- package/lib/git-helpers.js +109 -0
- package/lib/hook-templates/pre-commit.sh +54 -0
- package/lib/hook-templates/pre-push.sh +72 -0
- package/lib/install-hooks.js +205 -0
- package/lib/knowledge-graph.js +188 -0
- package/lib/learnings-audit.js +254 -0
- package/lib/learnings-parse.js +331 -0
- package/lib/learnings-sync.js +75 -0
- package/lib/mcp-detect.js +154 -0
- package/lib/metrics-collect.js +214 -0
- package/lib/overlay-merge.js +267 -0
- package/lib/performance-analyzer.js +142 -0
- package/lib/pipeline-config.js +83 -0
- package/lib/pipeline-status.js +207 -0
- package/lib/pipeline-validate.js +322 -0
- package/lib/platform-detect.js +86 -0
- package/lib/platform-discover.js +334 -0
- package/lib/publish-learning.js +160 -0
- package/lib/python-install.js +84 -0
- package/lib/refresh-check.js +67 -0
- package/lib/refresh-knowledge.js +360 -0
- package/lib/rule-resolver.js +146 -0
- package/lib/security-scanner.js +168 -0
- package/lib/setup-grounding.js +138 -0
- package/lib/skill-assertions.js +276 -0
- package/lib/skill-audit-render.js +158 -0
- package/lib/skill-audit.js +391 -0
- package/lib/stack-detect.js +170 -0
- package/lib/stack-paths.js +285 -0
- package/lib/story-classifier-extract.js +203 -0
- package/lib/story-classifier.js +282 -0
- package/lib/sync-overwrite.js +47 -0
- package/lib/synthetic-pipeline.js +299 -0
- package/lib/validate-metadata.js +175 -0
- package/package.json +41 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* doctor-docs.js — Hone doctor's documentation-freshness subcheck (H-017).
|
|
4
|
+
*
|
|
5
|
+
* Pure module: parses a README's test-count badge + inline mentions, compares
|
|
6
|
+
* against an actual test count from the project's test runner. Returns a
|
|
7
|
+
* structured result. Caller (hone-cli.js) does the I/O.
|
|
8
|
+
*
|
|
9
|
+
* H-016 / E20-A-L1 / E22-C pattern: pure function + thin shell.
|
|
10
|
+
*
|
|
11
|
+
* The OptionsFlow pilot wrote a Python equivalent (scripts/doc_freshness_check.py)
|
|
12
|
+
* before this CLI version existed. Logic and regex literals match that script
|
|
13
|
+
* so behavior is identical across the two implementations.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Matches shields.io badge variations:
|
|
17
|
+
// /tests-260%20passing-/ (URL-encoded space)
|
|
18
|
+
// /tests-260-passing-/ (single hyphen)
|
|
19
|
+
// /tests--260--passing-/ (double hyphen)
|
|
20
|
+
const BADGE_RE = /tests-{1,2}(\d+)(?:%20|-{1,2})passing/i;
|
|
21
|
+
|
|
22
|
+
// Matches inline prose like "260 tests passing" or "**260 tests passing**".
|
|
23
|
+
// Used to catch drifted counts in the README body, not just the badge.
|
|
24
|
+
const INLINE_RE = /\*{0,2}(\d+)\s+tests?\s+passing\*{0,2}/i;
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} readmeText — full contents of README.md
|
|
29
|
+
* @returns {number|null} the test count from the badge, or null if not found
|
|
30
|
+
*/
|
|
31
|
+
function extractBadgeCount(readmeText) {
|
|
32
|
+
const m = (readmeText || '').match(BADGE_RE);
|
|
33
|
+
return m ? parseInt(m[1], 10) : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} readmeText
|
|
38
|
+
* @returns {number[]} all inline test-count mentions in prose (excluding the badge)
|
|
39
|
+
*/
|
|
40
|
+
function extractInlineCounts(readmeText) {
|
|
41
|
+
const stripped = (readmeText || '').replace(BADGE_RE, '');
|
|
42
|
+
const matches = [];
|
|
43
|
+
let m;
|
|
44
|
+
const re = new RegExp(INLINE_RE, 'gi');
|
|
45
|
+
while ((m = re.exec(stripped)) !== null) {
|
|
46
|
+
matches.push(parseInt(m[1], 10));
|
|
47
|
+
}
|
|
48
|
+
return matches;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Evaluate the docs-freshness rule.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} input
|
|
56
|
+
* @param {string|null} input.readmeText — README.md contents (null if missing)
|
|
57
|
+
* @param {number|null} input.actualTestCount — test count from runner (null if runner unavailable)
|
|
58
|
+
* @returns {{
|
|
59
|
+
* status: 'ok' | 'drift' | 'skip',
|
|
60
|
+
* reason: string,
|
|
61
|
+
* badge: number|null,
|
|
62
|
+
* inline: number[],
|
|
63
|
+
* actual: number|null,
|
|
64
|
+
* suggestedFix: string|null,
|
|
65
|
+
* }}
|
|
66
|
+
*/
|
|
67
|
+
function checkDocsFreshness(input) {
|
|
68
|
+
const { readmeText, actualTestCount } = input || {};
|
|
69
|
+
|
|
70
|
+
if (readmeText == null) {
|
|
71
|
+
return {
|
|
72
|
+
status: 'skip',
|
|
73
|
+
reason: 'No README.md found at repo root.',
|
|
74
|
+
badge: null,
|
|
75
|
+
inline: [],
|
|
76
|
+
actual: actualTestCount ?? null,
|
|
77
|
+
suggestedFix: null,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const badge = extractBadgeCount(readmeText);
|
|
82
|
+
const inline = extractInlineCounts(readmeText);
|
|
83
|
+
|
|
84
|
+
if (badge == null && inline.length === 0) {
|
|
85
|
+
return {
|
|
86
|
+
status: 'skip',
|
|
87
|
+
reason: 'README.md has no test-count badge or inline mention; nothing to check.',
|
|
88
|
+
badge: null,
|
|
89
|
+
inline: [],
|
|
90
|
+
actual: actualTestCount ?? null,
|
|
91
|
+
suggestedFix: null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (actualTestCount == null) {
|
|
96
|
+
return {
|
|
97
|
+
status: 'skip',
|
|
98
|
+
reason:
|
|
99
|
+
'Could not determine actual test count (test runner unavailable or returned no count). ' +
|
|
100
|
+
'README mentions a count but cannot be verified.',
|
|
101
|
+
badge,
|
|
102
|
+
inline,
|
|
103
|
+
actual: null,
|
|
104
|
+
suggestedFix: null,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Compare badge + every inline mention against actual.
|
|
109
|
+
const mismatches = [];
|
|
110
|
+
if (badge != null && badge !== actualTestCount) {
|
|
111
|
+
mismatches.push(`badge says ${badge}`);
|
|
112
|
+
}
|
|
113
|
+
for (const c of inline) {
|
|
114
|
+
if (c !== actualTestCount) {
|
|
115
|
+
mismatches.push(`inline says ${c}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (mismatches.length === 0) {
|
|
120
|
+
return {
|
|
121
|
+
status: 'ok',
|
|
122
|
+
reason: `README test count matches actual (${actualTestCount}).`,
|
|
123
|
+
badge,
|
|
124
|
+
inline,
|
|
125
|
+
actual: actualTestCount,
|
|
126
|
+
suggestedFix: null,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
status: 'drift',
|
|
132
|
+
reason:
|
|
133
|
+
`README test count drifted: ${mismatches.join(', ')}, ` +
|
|
134
|
+
`but the test runner reports ${actualTestCount}.`,
|
|
135
|
+
badge,
|
|
136
|
+
inline,
|
|
137
|
+
actual: actualTestCount,
|
|
138
|
+
suggestedFix:
|
|
139
|
+
`Update README.md so every test-count mention reads ${actualTestCount}, or ` +
|
|
140
|
+
`re-run the check after the next test suite changes settle.`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Build the command to count tests for a given stack. Returns a shell command
|
|
147
|
+
* string that, when executed, prints "<N> tests collected" or similar — caller
|
|
148
|
+
* parses the output. Pure function — no I/O.
|
|
149
|
+
*
|
|
150
|
+
* Stacks with no known runner return null; caller should skip with a "skip" result.
|
|
151
|
+
*/
|
|
152
|
+
function testCounterCommand(stack) {
|
|
153
|
+
switch (stack) {
|
|
154
|
+
case 'python':
|
|
155
|
+
// pytest works for any Python project that has pytest installed
|
|
156
|
+
return 'python3 -m pytest --collect-only -q 2>&1 | tail -3';
|
|
157
|
+
case 'node':
|
|
158
|
+
case 'javascript':
|
|
159
|
+
case 'typescript':
|
|
160
|
+
// jest --listTests outputs one path per line; vitest is similar
|
|
161
|
+
// Use a generic strategy: try jest first, fall back to vitest list
|
|
162
|
+
return null; // explicit skip — Node test counts vary too much by toolchain
|
|
163
|
+
case 'go':
|
|
164
|
+
return "go test -list '.*' ./... 2>&1 | grep -cE '^(Test|Benchmark)'";
|
|
165
|
+
case 'rust':
|
|
166
|
+
return "cargo test -- --list 2>&1 | grep -cE '^test '";
|
|
167
|
+
default:
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Parse the output of a test-counter command into an integer count.
|
|
175
|
+
* Stack-specific because each runner formats output differently.
|
|
176
|
+
*/
|
|
177
|
+
function parseTestCount(stack, stdout) {
|
|
178
|
+
if (!stdout) return null;
|
|
179
|
+
switch (stack) {
|
|
180
|
+
case 'python': {
|
|
181
|
+
const m = stdout.match(/(\d+)\s+tests?\s+(?:collected|selected)/);
|
|
182
|
+
return m ? parseInt(m[1], 10) : null;
|
|
183
|
+
}
|
|
184
|
+
case 'go':
|
|
185
|
+
case 'rust': {
|
|
186
|
+
// Output is a count from grep -c
|
|
187
|
+
const n = parseInt(stdout.trim(), 10);
|
|
188
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
189
|
+
}
|
|
190
|
+
default:
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
checkDocsFreshness,
|
|
198
|
+
extractBadgeCount,
|
|
199
|
+
extractInlineCounts,
|
|
200
|
+
testCounterCommand,
|
|
201
|
+
parseTestCount,
|
|
202
|
+
// Exported for tests
|
|
203
|
+
BADGE_RE,
|
|
204
|
+
INLINE_RE,
|
|
205
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* doctor-placeholders.js — HC-002.
|
|
4
|
+
*
|
|
5
|
+
* Scans CI workflows + adopter config files for unsubstituted placeholders
|
|
6
|
+
* (per E21-A-L1, absorbed into pr-review-standards/SKILL.md §Adoption-time
|
|
7
|
+
* placeholder substitution is silent-disable vector).
|
|
8
|
+
*
|
|
9
|
+
* Detects placeholder formats:
|
|
10
|
+
* - {{PLACEHOLDER}} (Jinja / Handlebars-style)
|
|
11
|
+
* - <PLACEHOLDER> (XML-style, used in some YAML scaffolds)
|
|
12
|
+
* - __PLACEHOLDER__ (underscore-wrap, used in some shell scaffolds)
|
|
13
|
+
*
|
|
14
|
+
* Scans only the files where adopter substitution actually happens:
|
|
15
|
+
* - .github/workflows/*.yml
|
|
16
|
+
* - .github/.pipeline-config.yml
|
|
17
|
+
* - scripts/*.sh
|
|
18
|
+
* - copilot-instructions.md / CLAUDE.md (rare but possible)
|
|
19
|
+
*
|
|
20
|
+
* Returns conventional doctor result shape. Never throws.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
|
|
26
|
+
const PLACEHOLDER_PATTERNS = [
|
|
27
|
+
// {{NAME}} — jinja-style
|
|
28
|
+
{ re: /\{\{([A-Z_][A-Z0-9_]*)\}\}/g, format: '{{NAME}}' },
|
|
29
|
+
// <PLACEHOLDER_NAME> — XML-bracketed (only when ALL_CAPS to avoid HTML / XML false positives)
|
|
30
|
+
{ re: /<([A-Z_][A-Z0-9_]{2,})>/g, format: '<NAME>' },
|
|
31
|
+
// __PLACEHOLDER_NAME__ — underscore-wrapped
|
|
32
|
+
{ re: /__([A-Z_][A-Z0-9_]{2,})__/g, format: '__NAME__' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const SCAN_PATHS = [
|
|
36
|
+
'.github/workflows',
|
|
37
|
+
'.github/.pipeline-config.yml',
|
|
38
|
+
'.github/copilot-instructions.md',
|
|
39
|
+
'CLAUDE.md',
|
|
40
|
+
'scripts',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Known false positives to ignore (legitimate uses of <X> / __NAME__ / etc.)
|
|
44
|
+
const ALLOW_LIST = [
|
|
45
|
+
'<NAME>',
|
|
46
|
+
'<URL>',
|
|
47
|
+
'<EMAIL>',
|
|
48
|
+
'<USER>',
|
|
49
|
+
'<TOKEN>',
|
|
50
|
+
'<PROJECT>',
|
|
51
|
+
'<BRANCH>',
|
|
52
|
+
'<STORY-ID>', // doc syntax: paths like .github/pipeline/<STORY-ID>/...
|
|
53
|
+
'<id>',
|
|
54
|
+
'<n>',
|
|
55
|
+
'__init__',
|
|
56
|
+
'__main__',
|
|
57
|
+
'__name__',
|
|
58
|
+
'__future__',
|
|
59
|
+
'__file__',
|
|
60
|
+
'__dict__',
|
|
61
|
+
'__doc__',
|
|
62
|
+
'__class__',
|
|
63
|
+
'__version__',
|
|
64
|
+
'__all__',
|
|
65
|
+
'__future__',
|
|
66
|
+
'__pycache__',
|
|
67
|
+
'__future__',
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {object} args
|
|
72
|
+
* @param {string} args.repoRoot
|
|
73
|
+
* @returns {{ name: 'placeholders', status: 'ok'|'drift'|'skip', reason: string, suggestedFix?: string }}
|
|
74
|
+
*/
|
|
75
|
+
function checkPlaceholders(args) {
|
|
76
|
+
const { repoRoot } = args || {};
|
|
77
|
+
const name = 'placeholders';
|
|
78
|
+
|
|
79
|
+
if (!repoRoot || typeof repoRoot !== 'string') {
|
|
80
|
+
return { name, status: 'skip', reason: 'no repoRoot supplied' };
|
|
81
|
+
}
|
|
82
|
+
if (!fs.existsSync(repoRoot)) {
|
|
83
|
+
return { name, status: 'skip', reason: `repoRoot does not exist: ${repoRoot}` };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const offenders = [];
|
|
87
|
+
for (const relPath of SCAN_PATHS) {
|
|
88
|
+
const fullPath = path.join(repoRoot, relPath);
|
|
89
|
+
if (!fs.existsSync(fullPath)) continue;
|
|
90
|
+
|
|
91
|
+
const stat = fs.statSync(fullPath);
|
|
92
|
+
if (stat.isFile()) {
|
|
93
|
+
scanFile(fullPath, relPath, offenders);
|
|
94
|
+
} else if (stat.isDirectory()) {
|
|
95
|
+
// Recurse one level deep into known dirs
|
|
96
|
+
for (const entry of fs.readdirSync(fullPath)) {
|
|
97
|
+
const ep = path.join(fullPath, entry);
|
|
98
|
+
if (fs.statSync(ep).isFile()) {
|
|
99
|
+
scanFile(ep, path.join(relPath, entry), offenders);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (offenders.length === 0) {
|
|
106
|
+
return {
|
|
107
|
+
name,
|
|
108
|
+
status: 'ok',
|
|
109
|
+
reason: 'no unsubstituted placeholders found in scanned paths',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const detail = offenders.slice(0, 5)
|
|
114
|
+
.map(o => `${o.file}:${o.line} (${o.placeholder})`)
|
|
115
|
+
.join(', ');
|
|
116
|
+
return {
|
|
117
|
+
name,
|
|
118
|
+
status: 'drift',
|
|
119
|
+
reason: `Unsubstituted placeholders detected — ${offenders.length} violation(s): ${detail}${offenders.length > 5 ? ` (+${offenders.length - 5} more)` : ''}. Adoption-time placeholder substitution is a silent-disable vector (per E21-A-L1).`,
|
|
120
|
+
suggestedFix: 'Re-run `hone setup` or `hone adopt` to substitute, or hand-edit the files. See pr-review-standards/SKILL.md §Adoption-time placeholder substitution.',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function scanFile(absPath, relPath, offenders) {
|
|
125
|
+
let content;
|
|
126
|
+
try { content = fs.readFileSync(absPath, 'utf8'); }
|
|
127
|
+
catch { return; }
|
|
128
|
+
|
|
129
|
+
const lines = content.split('\n');
|
|
130
|
+
for (let i = 0; i < lines.length; i++) {
|
|
131
|
+
const line = lines[i];
|
|
132
|
+
for (const { re, format } of PLACEHOLDER_PATTERNS) {
|
|
133
|
+
re.lastIndex = 0;
|
|
134
|
+
let m;
|
|
135
|
+
while ((m = re.exec(line)) !== null) {
|
|
136
|
+
const placeholder = m[0];
|
|
137
|
+
if (ALLOW_LIST.includes(placeholder)) continue;
|
|
138
|
+
offenders.push({ file: relPath, line: i + 1, placeholder, format });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = { checkPlaceholders, PLACEHOLDER_PATTERNS, SCAN_PATHS };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* doctor-skill-staleness.js — HC-002.
|
|
4
|
+
*
|
|
5
|
+
* Surfaces skill staleness in the `hone doctor` summary (per E16-A-L4,
|
|
6
|
+
* absorbed into observability/SKILL.md §11 Skill-staleness disclosure).
|
|
7
|
+
*
|
|
8
|
+
* Reads `.github/.pipeline-config.yml` for `skill_refresh.last_derived`,
|
|
9
|
+
* compares against age threshold (default 30 days). Optional growth check:
|
|
10
|
+
* if .github/skills/ source-file count grew >50% since last_derived, flag
|
|
11
|
+
* as stale even if within age threshold.
|
|
12
|
+
*
|
|
13
|
+
* Existing `hone check-refresh` command already does similar work; this
|
|
14
|
+
* helper mirrors its logic so doctor's summary includes the staleness
|
|
15
|
+
* signal alongside other health checks.
|
|
16
|
+
*
|
|
17
|
+
* Returns conventional doctor result shape. Never throws.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {object} args
|
|
25
|
+
* @param {string} args.repoRoot
|
|
26
|
+
* @param {number} [args.ageThresholdDays=30]
|
|
27
|
+
* @param {number} [args.growthPctThreshold=50]
|
|
28
|
+
* @returns {{ name: 'skill-staleness', status: 'ok'|'warn'|'skip', reason: string, suggestedFix?: string }}
|
|
29
|
+
*/
|
|
30
|
+
function checkSkillStaleness(args) {
|
|
31
|
+
const { repoRoot, ageThresholdDays = 30, growthPctThreshold = 50 } = args || {};
|
|
32
|
+
const name = 'skill-staleness';
|
|
33
|
+
|
|
34
|
+
if (!repoRoot || typeof repoRoot !== 'string') {
|
|
35
|
+
return { name, status: 'skip', reason: 'no repoRoot supplied' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Try both .github/.pipeline-config.yml and .pipeline-config.yml (per SC-003 dual-location)
|
|
39
|
+
const candidates = [
|
|
40
|
+
path.join(repoRoot, '.github/.pipeline-config.yml'),
|
|
41
|
+
path.join(repoRoot, '.pipeline-config.yml'),
|
|
42
|
+
];
|
|
43
|
+
const cfgPath = candidates.find(p => fs.existsSync(p));
|
|
44
|
+
if (!cfgPath) {
|
|
45
|
+
return { name, status: 'skip', reason: 'no .pipeline-config.yml found (skipping staleness check)' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let cfg;
|
|
49
|
+
try {
|
|
50
|
+
const yaml = require('js-yaml');
|
|
51
|
+
cfg = yaml.load(fs.readFileSync(cfgPath, 'utf8'), { schema: yaml.JSON_SCHEMA });
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return { name, status: 'skip', reason: `failed to parse ${cfgPath}: ${e.message}` };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const lastDerived = cfg?.skill_refresh?.last_derived;
|
|
57
|
+
if (!lastDerived) {
|
|
58
|
+
return {
|
|
59
|
+
name,
|
|
60
|
+
status: 'skip',
|
|
61
|
+
reason: 'no skill_refresh.last_derived recorded (run `hone derive` to initialize)',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Age check
|
|
66
|
+
const lastDerivedDate = new Date(lastDerived);
|
|
67
|
+
if (isNaN(lastDerivedDate.getTime())) {
|
|
68
|
+
return { name, status: 'skip', reason: `invalid skill_refresh.last_derived: ${lastDerived}` };
|
|
69
|
+
}
|
|
70
|
+
const ageDays = Math.floor((Date.now() - lastDerivedDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
71
|
+
|
|
72
|
+
// Growth check (optional — count files in .github/skills/)
|
|
73
|
+
let growthPct = null;
|
|
74
|
+
const skillsDir = path.join(repoRoot, '.github/skills');
|
|
75
|
+
if (fs.existsSync(skillsDir)) {
|
|
76
|
+
const fileCount = countFiles(skillsDir);
|
|
77
|
+
const recordedCount = cfg?.skill_refresh?.source_file_count;
|
|
78
|
+
if (typeof recordedCount === 'number' && recordedCount > 0) {
|
|
79
|
+
growthPct = Math.round(((fileCount - recordedCount) / recordedCount) * 100);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ageStale = ageDays > ageThresholdDays;
|
|
84
|
+
const growthStale = growthPct !== null && growthPct > growthPctThreshold;
|
|
85
|
+
|
|
86
|
+
if (!ageStale && !growthStale) {
|
|
87
|
+
return {
|
|
88
|
+
name,
|
|
89
|
+
status: 'ok',
|
|
90
|
+
reason: `skills derived ${ageDays} days ago (within ${ageThresholdDays}-day threshold)${growthPct !== null ? `; source dir growth ${growthPct}%` : ''}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const reasons = [];
|
|
95
|
+
if (ageStale) reasons.push(`derived ${ageDays} days ago (>${ageThresholdDays})`);
|
|
96
|
+
if (growthStale) reasons.push(`source dir grew ${growthPct}% (>${growthPctThreshold}%)`);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
name,
|
|
100
|
+
status: 'warn',
|
|
101
|
+
reason: `Skills are stale: ${reasons.join('; ')}`,
|
|
102
|
+
suggestedFix: 'Run `hone derive` to refresh domain skills (or `hone refresh-knowledge` once HC-003 ships). See observability/SKILL.md §11 Skill-staleness disclosure.',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function countFiles(dir) {
|
|
107
|
+
let count = 0;
|
|
108
|
+
try {
|
|
109
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
110
|
+
const ep = path.join(dir, entry);
|
|
111
|
+
const stat = fs.statSync(ep);
|
|
112
|
+
if (stat.isDirectory()) {
|
|
113
|
+
count += countFiles(ep);
|
|
114
|
+
} else if (entry === 'SKILL.md') {
|
|
115
|
+
count += 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch {}
|
|
119
|
+
return count;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { checkSkillStaleness };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: {{DOMAIN_NAME}}-domain
|
|
3
|
+
description: >
|
|
4
|
+
Per-adopter domain skill for {{DOMAIN_NAME}}. Encodes vocabulary,
|
|
5
|
+
architectural contracts, review rules, anti-hallucination guards,
|
|
6
|
+
and calibration unknowns specific to this domain. Hand-edit the
|
|
7
|
+
sections below; the framework will not overwrite this file unless
|
|
8
|
+
you remove the `managed_by` line and re-run with --force.
|
|
9
|
+
autoLoad: false
|
|
10
|
+
managed_by: hone-derive-domain
|
|
11
|
+
derived_at: "{{DERIVED_AT}}"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<!-- HC-004-MANAGED-MARKER — DO NOT EDIT this comment; it lets `hone derive-domain` detect this is a framework-scaffolded file -->
|
|
15
|
+
|
|
16
|
+
# {{DOMAIN_NAME}} Domain Skill
|
|
17
|
+
|
|
18
|
+
> Generated by `hone derive-domain {{DOMAIN_NAME}}` (HC-004) using the
|
|
19
|
+
> 7-section template documented in `pipeline-integrity §13` (Per-adopter
|
|
20
|
+
> `<domain>-domain/SKILL.md` slot, OptionsFlow E26-A-L1).
|
|
21
|
+
>
|
|
22
|
+
> Each section below ships with a placeholder + a guidance comment.
|
|
23
|
+
> Replace placeholders with your domain's actual content; keep the
|
|
24
|
+
> 7-section structure so cross-references from agent prompts work.
|
|
25
|
+
>
|
|
26
|
+
> When you're satisfied with content, set `autoLoad: true` in the
|
|
27
|
+
> frontmatter so this skill loads automatically alongside the
|
|
28
|
+
> generic-engineering skills.
|
|
29
|
+
|
|
30
|
+
## §1 Vocabulary
|
|
31
|
+
|
|
32
|
+
<!-- guidance: list domain terms with their PRECISE meaning. Distinguish overloaded terms (e.g., "delta" in options has 3 meanings: greek, change-amount, status indicator). Disambiguate at first use; cross-reference where the definition lives. -->
|
|
33
|
+
|
|
34
|
+
- **<TERM-1>** — <one-line precise meaning. Note any overload with other domains>
|
|
35
|
+
- **<TERM-2>** — <...>
|
|
36
|
+
- **<TERM-N>** — <...>
|
|
37
|
+
|
|
38
|
+
## §2 Architectural Contracts
|
|
39
|
+
|
|
40
|
+
<!-- guidance: list MUST-DO rules domain-specific to this domain. Data flow, calculation conventions (e.g., dealer-gamma sign), regulatory constraints (e.g., HIPAA logging), trade-cutoff times, etc. Each rule should be testable AND have a Code Reviewer-actionable enforcement. -->
|
|
41
|
+
|
|
42
|
+
- **Contract 1**: <e.g., "All position-sizing calculations MUST use the trading-day calendar, never the calendar day">
|
|
43
|
+
- **Contract 2**: <...>
|
|
44
|
+
- **Contract N**: <...>
|
|
45
|
+
|
|
46
|
+
## §3 Review Rules
|
|
47
|
+
|
|
48
|
+
<!-- guidance: what does Code Reviewer flag in domain code? Map each Architectural Contract above to a concrete review check. Use prescriptive language (per pr-review-standards/SKILL.md §Make hallucination IMPOSSIBLE) — "MUST NOT", "Never", "Forbid". -->
|
|
49
|
+
|
|
50
|
+
For any PR that modifies <domain> code, Code Reviewer flags:
|
|
51
|
+
|
|
52
|
+
- **<Rule 1>**: <e.g., "Position sizing without trading-day calendar import → HIGH">
|
|
53
|
+
- **<Rule 2>**: <...>
|
|
54
|
+
- **<Rule N>**: <...>
|
|
55
|
+
|
|
56
|
+
## §4 Anti-Hallucination Rules
|
|
57
|
+
|
|
58
|
+
<!-- guidance: per SC-009 §Make hallucination IMPOSSIBLE not discouraged (E20-A-L3), AI/scoring features in this domain MUST cite traceable sources. Every non-zero score / claim / inference must reference a source field (API response, paid-feed row, ground-truth lookup). Rule the rule prescriptively in the language. -->
|
|
59
|
+
|
|
60
|
+
For any AI / ML / scoring feature in <domain>:
|
|
61
|
+
|
|
62
|
+
- **MUST cite traceable source**: every non-zero score / claim / inference references a field in the input (e.g., `whales[symbol].volume`, `compliance_record.id`, `lab_result.timestamp`)
|
|
63
|
+
- **NEVER emit ungrounded claims**: stub functions return deterministic empty (`score: 0`, `direction: None`); only follow-up implementation activates real signals
|
|
64
|
+
- **<Domain-specific anti-hallucination rule>**: <...>
|
|
65
|
+
|
|
66
|
+
Cross-reference: `pr-review-standards/SKILL.md` §Make hallucination IMPOSSIBLE not discouraged.
|
|
67
|
+
|
|
68
|
+
## §5 Calibration & Empirical Unknowns
|
|
69
|
+
|
|
70
|
+
<!-- guidance: per pipeline-integrity §14 calibration unknowns documented in domain skills (E26-A-L2), every tunable threshold MUST be flagged as UNKNOWN until measured. List each constant with its current value, status (UNKNOWN / MEASURED / EVIDENCE-BACKED), and a deferred-work tracker. -->
|
|
71
|
+
|
|
72
|
+
The constants below are CURRENT VALUES, not EVIDENCE-BACKED. PRs that
|
|
73
|
+
change a value MUST cite the measurement that justifies the new value.
|
|
74
|
+
|
|
75
|
+
| Constant | Current value | Status | Deferred work |
|
|
76
|
+
|---|---|---|---|
|
|
77
|
+
| <e.g. confidence_threshold> | 0.65 | UNKNOWN | <ticket-id>: backtest 90 days → tune |
|
|
78
|
+
| <e.g. consensus_rule> | 3-of-5 | UNKNOWN | <ticket-id>: simulate alternatives |
|
|
79
|
+
| ... | ... | UNKNOWN | ... |
|
|
80
|
+
|
|
81
|
+
Cross-reference: `pipeline-integrity/SKILL.md` §14 Calibration unknowns documented in domain skills.
|
|
82
|
+
|
|
83
|
+
## §6 Known Asymmetries / Bugs
|
|
84
|
+
|
|
85
|
+
<!-- guidance: domain-specific gotchas + their workarounds. Sign-convention footguns (per architecture-patterns §Domain-convention-vs-intuition footgun), structural biases, edge cases that bit production once. Each item should explain the trap + the mitigation. -->
|
|
86
|
+
|
|
87
|
+
- **<Asymmetry / Bug 1>**: <description>
|
|
88
|
+
- **Workaround**: <how to avoid it>
|
|
89
|
+
- **<Asymmetry / Bug 2>**: <...>
|
|
90
|
+
|
|
91
|
+
Cross-reference: `architecture-patterns/SKILL.md` §Symmetry in multi-layer scoring systems (and §Domain-convention-vs-intuition footgun for sign conventions).
|
|
92
|
+
|
|
93
|
+
## §7 References
|
|
94
|
+
|
|
95
|
+
<!-- guidance: canonical sources, regulators, papers, related domain skills, internal docs. Anything a future contributor needs to look up to understand this domain. -->
|
|
96
|
+
|
|
97
|
+
- **Canonical source**: <URL or paper citation>
|
|
98
|
+
- **Regulator**: <e.g., SEC Rule X, GDPR Article Y, HIPAA §...>
|
|
99
|
+
- **Related domain skills**: <e.g., `compliance-domain/SKILL.md`, `risk-domain/SKILL.md`>
|
|
100
|
+
- **Internal docs**: <e.g., `docs/architecture/<domain>-overview.md`>
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
> **Editing this file**:
|
|
105
|
+
> - Hand-edit any section above. The `managed_by: hone-derive-domain`
|
|
106
|
+
> line keeps this file in framework-managed state; re-running
|
|
107
|
+
> `hone derive-domain {{DOMAIN_NAME}}` is a no-op while the marker
|
|
108
|
+
> is present.
|
|
109
|
+
> - To take full ownership and prevent the framework from ever
|
|
110
|
+
> touching this file: remove the `managed_by` line. Future
|
|
111
|
+
> `hone derive-domain` runs (and `--force` runs) will refuse to
|
|
112
|
+
> overwrite without an explicit `--force` flag from you.
|
|
113
|
+
> - To regenerate from the latest framework template: run
|
|
114
|
+
> `hone derive-domain {{DOMAIN_NAME}} --force` (your edits will be lost).
|