@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.
Files changed (60) hide show
  1. package/bin/hone.js +2 -0
  2. package/hone-cli.js +4006 -0
  3. package/lib/README.md +119 -0
  4. package/lib/adversarial-negative-lint.js +149 -0
  5. package/lib/audit.js +156 -0
  6. package/lib/auto-detect.js +213 -0
  7. package/lib/autofix-guardrails.js +124 -0
  8. package/lib/branch-protection.js +256 -0
  9. package/lib/ci-classifier.js +150 -0
  10. package/lib/ci-failures.js +173 -0
  11. package/lib/claude-md-tokens.js +71 -0
  12. package/lib/compliance-check.js +62 -0
  13. package/lib/config-augment.js +133 -0
  14. package/lib/config-update.js +70 -0
  15. package/lib/dependency-audit.js +108 -0
  16. package/lib/derive-domain.js +185 -0
  17. package/lib/doc-registry.js +63 -0
  18. package/lib/doctor-admin-merge.js +185 -0
  19. package/lib/doctor-bind-default.js +118 -0
  20. package/lib/doctor-docs.js +205 -0
  21. package/lib/doctor-placeholders.js +144 -0
  22. package/lib/doctor-skill-staleness.js +122 -0
  23. package/lib/domain-skill-template.md +114 -0
  24. package/lib/editor-detect.js +169 -0
  25. package/lib/fast-track-ratify.js +133 -0
  26. package/lib/git-helpers.js +109 -0
  27. package/lib/hook-templates/pre-commit.sh +54 -0
  28. package/lib/hook-templates/pre-push.sh +72 -0
  29. package/lib/install-hooks.js +205 -0
  30. package/lib/knowledge-graph.js +188 -0
  31. package/lib/learnings-audit.js +254 -0
  32. package/lib/learnings-parse.js +331 -0
  33. package/lib/learnings-sync.js +75 -0
  34. package/lib/mcp-detect.js +154 -0
  35. package/lib/metrics-collect.js +214 -0
  36. package/lib/overlay-merge.js +267 -0
  37. package/lib/performance-analyzer.js +142 -0
  38. package/lib/pipeline-config.js +83 -0
  39. package/lib/pipeline-status.js +207 -0
  40. package/lib/pipeline-validate.js +322 -0
  41. package/lib/platform-detect.js +86 -0
  42. package/lib/platform-discover.js +334 -0
  43. package/lib/publish-learning.js +160 -0
  44. package/lib/python-install.js +84 -0
  45. package/lib/refresh-check.js +67 -0
  46. package/lib/refresh-knowledge.js +360 -0
  47. package/lib/rule-resolver.js +146 -0
  48. package/lib/security-scanner.js +168 -0
  49. package/lib/setup-grounding.js +138 -0
  50. package/lib/skill-assertions.js +276 -0
  51. package/lib/skill-audit-render.js +158 -0
  52. package/lib/skill-audit.js +391 -0
  53. package/lib/stack-detect.js +170 -0
  54. package/lib/stack-paths.js +285 -0
  55. package/lib/story-classifier-extract.js +203 -0
  56. package/lib/story-classifier.js +282 -0
  57. package/lib/sync-overwrite.js +47 -0
  58. package/lib/synthetic-pipeline.js +299 -0
  59. package/lib/validate-metadata.js +175 -0
  60. 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).