@temet/cli 0.2.0 → 0.3.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.
@@ -0,0 +1,55 @@
1
+ import type { AuditResult } from "./session-audit.js";
2
+ import type { CompetencyEntry, ProficiencyLevel } from "./types.js";
3
+ export type AuditSkillSnapshot = {
4
+ name: string;
5
+ category: string;
6
+ proficiencyLevel: ProficiencyLevel;
7
+ evidenceCount: number;
8
+ };
9
+ export type AuditWorkflowSnapshot = {
10
+ description: string;
11
+ sequence: string[];
12
+ occurrences: number;
13
+ sessions: number;
14
+ confidence: number;
15
+ };
16
+ export type AuditSnapshot = {
17
+ version: 1;
18
+ createdAt: string;
19
+ projectKey: string;
20
+ projectLabel: string;
21
+ sourcePath: string;
22
+ sessions: number;
23
+ messages: number;
24
+ toolCalls: number;
25
+ skills: AuditSkillSnapshot[];
26
+ workflows: AuditWorkflowSnapshot[];
27
+ };
28
+ export type AuditChange = {
29
+ type: "baseline";
30
+ title: string;
31
+ detail: string;
32
+ } | {
33
+ type: "new_skill" | "level_up" | "level_down" | "new_workflow";
34
+ title: string;
35
+ detail: string;
36
+ };
37
+ export type TrackingResult = {
38
+ previous: AuditSnapshot | null;
39
+ current: AuditSnapshot;
40
+ changes: AuditChange[];
41
+ latestPath: string;
42
+ historyPath: string;
43
+ skipped: boolean;
44
+ };
45
+ export declare function buildProjectKey(sourcePath: string): string;
46
+ export declare function buildAuditSnapshot(sourcePath: string, result: Pick<AuditResult, "sessionCount" | "messageCount" | "toolCallCount" | "workflows">, competencies: CompetencyEntry[]): AuditSnapshot;
47
+ export declare function loadLatestSnapshot(sourcePath: string): Promise<AuditSnapshot | null>;
48
+ export declare function saveAuditSnapshot(snapshot: AuditSnapshot): Promise<{
49
+ latestPath: string;
50
+ historyPath: string;
51
+ }>;
52
+ export declare function diffAuditSnapshots(previous: AuditSnapshot | null, current: AuditSnapshot): AuditChange[];
53
+ /** Check if two snapshots have identical skills and workflows (ignoring timestamps/counts). */
54
+ export declare function snapshotsEqual(a: AuditSnapshot, b: AuditSnapshot): boolean;
55
+ export declare function trackAuditSnapshot(sourcePath: string, result: Pick<AuditResult, "sessionCount" | "messageCount" | "toolCallCount" | "workflows">, competencies: CompetencyEntry[]): Promise<TrackingResult>;
@@ -0,0 +1,185 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import path from "node:path";
5
+ const PROFICIENCY_ORDER = {
6
+ novice: 0,
7
+ advanced_beginner: 1,
8
+ competent: 2,
9
+ proficient: 3,
10
+ expert: 4,
11
+ };
12
+ function normalizeName(value) {
13
+ return value.toLowerCase().replace(/\s+/g, " ").trim();
14
+ }
15
+ function evidenceCount(entry) {
16
+ return (entry.evidence.examples.length +
17
+ entry.evidence.decisionCriteria.length +
18
+ entry.evidence.antiPatterns.length +
19
+ entry.evidence.mentorAdvice.length);
20
+ }
21
+ export function buildProjectKey(sourcePath) {
22
+ return createHash("sha256")
23
+ .update(path.resolve(sourcePath))
24
+ .digest("hex")
25
+ .slice(0, 12);
26
+ }
27
+ function buildProjectLabel(sourcePath) {
28
+ const resolved = path.resolve(sourcePath);
29
+ const base = path.basename(resolved);
30
+ return base || resolved;
31
+ }
32
+ export function buildAuditSnapshot(sourcePath, result, competencies) {
33
+ return {
34
+ version: 1,
35
+ createdAt: new Date().toISOString(),
36
+ projectKey: buildProjectKey(sourcePath),
37
+ projectLabel: buildProjectLabel(sourcePath),
38
+ sourcePath: path.resolve(sourcePath),
39
+ sessions: result.sessionCount,
40
+ messages: result.messageCount,
41
+ toolCalls: result.toolCallCount,
42
+ skills: competencies
43
+ .map((entry) => ({
44
+ name: entry.name,
45
+ category: entry.category,
46
+ proficiencyLevel: entry.proficiencyLevel,
47
+ evidenceCount: evidenceCount(entry),
48
+ }))
49
+ .sort((a, b) => a.name.localeCompare(b.name)),
50
+ workflows: result.workflows
51
+ .map((workflow) => ({
52
+ description: workflow.description,
53
+ sequence: workflow.sequence,
54
+ occurrences: workflow.occurrences,
55
+ sessions: workflow.sessions,
56
+ confidence: workflow.confidence,
57
+ }))
58
+ .sort((a, b) => a.description.localeCompare(b.description)),
59
+ };
60
+ }
61
+ function getTrackingPaths(projectKey) {
62
+ const root = path.join(homedir(), ".temet", "audits", projectKey);
63
+ return {
64
+ root,
65
+ latest: path.join(root, "latest.json"),
66
+ historyDir: path.join(root, "history"),
67
+ };
68
+ }
69
+ export async function loadLatestSnapshot(sourcePath) {
70
+ const { latest } = getTrackingPaths(buildProjectKey(sourcePath));
71
+ try {
72
+ const raw = await readFile(latest, "utf8");
73
+ const parsed = JSON.parse(raw);
74
+ if (parsed?.version !== 1)
75
+ return null;
76
+ return parsed;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ export async function saveAuditSnapshot(snapshot) {
83
+ const paths = getTrackingPaths(snapshot.projectKey);
84
+ await mkdir(paths.historyDir, { recursive: true });
85
+ const historyPath = path.join(paths.historyDir, `${snapshot.createdAt.replaceAll(":", "-")}.json`);
86
+ const payload = JSON.stringify(snapshot, null, 2);
87
+ await writeFile(paths.latest, payload, "utf8");
88
+ await writeFile(historyPath, payload, "utf8");
89
+ return { latestPath: paths.latest, historyPath };
90
+ }
91
+ export function diffAuditSnapshots(previous, current) {
92
+ if (!previous) {
93
+ return [
94
+ {
95
+ type: "baseline",
96
+ title: "Tracking baseline created",
97
+ detail: `Saved the first audit snapshot for ${current.projectLabel}.`,
98
+ },
99
+ ];
100
+ }
101
+ const changes = [];
102
+ const previousSkills = new Map(previous.skills.map((skill) => [normalizeName(skill.name), skill]));
103
+ const currentSkills = new Map(current.skills.map((skill) => [normalizeName(skill.name), skill]));
104
+ for (const [key, skill] of currentSkills) {
105
+ const before = previousSkills.get(key);
106
+ if (!before) {
107
+ if (PROFICIENCY_ORDER[skill.proficiencyLevel] >=
108
+ PROFICIENCY_ORDER.competent ||
109
+ skill.evidenceCount >= 2) {
110
+ changes.push({
111
+ type: "new_skill",
112
+ title: `New skill surfaced: ${skill.name}`,
113
+ detail: `${skill.proficiencyLevel.replace("_", " ")} with ${skill.evidenceCount} evidence item(s).`,
114
+ });
115
+ }
116
+ continue;
117
+ }
118
+ const beforeLevel = PROFICIENCY_ORDER[before.proficiencyLevel];
119
+ const currentLevel = PROFICIENCY_ORDER[skill.proficiencyLevel];
120
+ if (currentLevel > beforeLevel) {
121
+ changes.push({
122
+ type: "level_up",
123
+ title: `${skill.name} moved up`,
124
+ detail: `${before.proficiencyLevel.replace("_", " ")} → ${skill.proficiencyLevel.replace("_", " ")}`,
125
+ });
126
+ }
127
+ else if (currentLevel < beforeLevel) {
128
+ changes.push({
129
+ type: "level_down",
130
+ title: `${skill.name} moved down`,
131
+ detail: `${before.proficiencyLevel.replace("_", " ")} → ${skill.proficiencyLevel.replace("_", " ")}`,
132
+ });
133
+ }
134
+ }
135
+ const previousWorkflows = new Set(previous.workflows.map((workflow) => normalizeName(workflow.description)));
136
+ for (const workflow of current.workflows) {
137
+ const key = normalizeName(workflow.description);
138
+ if (!previousWorkflows.has(key)) {
139
+ changes.push({
140
+ type: "new_workflow",
141
+ title: `New repeated pattern: ${workflow.description}`,
142
+ detail: `${workflow.occurrences} occurrences across ${workflow.sessions} sessions.`,
143
+ });
144
+ }
145
+ }
146
+ const order = {
147
+ baseline: 0,
148
+ level_up: 1,
149
+ new_skill: 2,
150
+ new_workflow: 3,
151
+ level_down: 4,
152
+ };
153
+ return changes.sort((a, b) => order[a.type] - order[b.type]).slice(0, 8);
154
+ }
155
+ /** Check if two snapshots have identical skills and workflows (ignoring timestamps/counts). */
156
+ export function snapshotsEqual(a, b) {
157
+ return (JSON.stringify(a.skills) === JSON.stringify(b.skills) &&
158
+ JSON.stringify(a.workflows) === JSON.stringify(b.workflows));
159
+ }
160
+ export async function trackAuditSnapshot(sourcePath, result, competencies) {
161
+ const previous = await loadLatestSnapshot(sourcePath);
162
+ const current = buildAuditSnapshot(sourcePath, result, competencies);
163
+ // No-op guard: skip save if skills and workflows are identical
164
+ if (previous && snapshotsEqual(previous, current)) {
165
+ const paths = getTrackingPaths(current.projectKey);
166
+ return {
167
+ previous,
168
+ current,
169
+ changes: [],
170
+ latestPath: paths.latest,
171
+ historyPath: "",
172
+ skipped: true,
173
+ };
174
+ }
175
+ const changes = diffAuditSnapshots(previous, current);
176
+ const { latestPath, historyPath } = await saveAuditSnapshot(current);
177
+ return {
178
+ previous,
179
+ current,
180
+ changes,
181
+ latestPath,
182
+ historyPath,
183
+ skipped: false,
184
+ };
185
+ }
@@ -0,0 +1,21 @@
1
+ export type FlagValue = string | boolean;
2
+ export type AuditCliOptions = {
3
+ path: string;
4
+ track: boolean;
5
+ narrate: boolean;
6
+ json: boolean;
7
+ quiet: boolean;
8
+ notify: boolean;
9
+ publish: boolean;
10
+ yes: boolean;
11
+ model: string;
12
+ address: string;
13
+ token: string;
14
+ relayUrl: string;
15
+ };
16
+ export declare function parseFlagBag(args: string[]): {
17
+ flags: Map<string, FlagValue>;
18
+ positionals: string[];
19
+ };
20
+ export declare function readOptionalString(flags: Map<string, FlagValue>, key: string): string | null;
21
+ export declare function buildAuditCliOptions(flags: Map<string, FlagValue>, env: NodeJS.ProcessEnv): AuditCliOptions;
@@ -0,0 +1,60 @@
1
+ export function parseFlagBag(args) {
2
+ const flags = new Map();
3
+ const positionals = [];
4
+ for (let i = 0; i < args.length; i++) {
5
+ const arg = args[i];
6
+ if (arg === "-y") {
7
+ flags.set("yes", true);
8
+ continue;
9
+ }
10
+ if (!arg.startsWith("--")) {
11
+ positionals.push(arg);
12
+ continue;
13
+ }
14
+ if (arg === "--dry-run" ||
15
+ arg === "--track" ||
16
+ arg === "--narrate" ||
17
+ arg === "--json" ||
18
+ arg === "--quiet" ||
19
+ arg === "--notify" ||
20
+ arg === "--publish" ||
21
+ arg === "--yes") {
22
+ flags.set(arg.slice(2), true);
23
+ continue;
24
+ }
25
+ const key = arg.slice(2);
26
+ const value = args[i + 1];
27
+ if (!value || value.startsWith("--")) {
28
+ throw new Error(`Missing value for --${key}`);
29
+ }
30
+ flags.set(key, value);
31
+ i += 1;
32
+ }
33
+ return { flags, positionals };
34
+ }
35
+ export function readOptionalString(flags, key) {
36
+ const value = flags.get(key);
37
+ if (typeof value !== "string")
38
+ return null;
39
+ const trimmed = value.trim();
40
+ return trimmed.length > 0 ? trimmed : null;
41
+ }
42
+ export function buildAuditCliOptions(flags, env) {
43
+ const pathVal = readOptionalString(flags, "path") ?? "";
44
+ return {
45
+ path: pathVal,
46
+ track: Boolean(flags.get("track")),
47
+ narrate: Boolean(flags.get("narrate")),
48
+ json: Boolean(flags.get("json")),
49
+ quiet: Boolean(flags.get("quiet")),
50
+ notify: Boolean(flags.get("notify")),
51
+ publish: Boolean(flags.get("publish")),
52
+ yes: Boolean(flags.get("yes")),
53
+ model: readOptionalString(flags, "model") ?? "",
54
+ address: readOptionalString(flags, "address") ?? env.TEMET_ADDRESS ?? "",
55
+ token: readOptionalString(flags, "token") ?? env.TEMET_TOKEN ?? "",
56
+ relayUrl: readOptionalString(flags, "relay-url") ??
57
+ env.RELAY_URL ??
58
+ "https://temet-relay.ramponneau.workers.dev",
59
+ };
60
+ }
@@ -0,0 +1,15 @@
1
+ import type { SessionStats } from "./session-parser.js";
2
+ export type Signal = {
3
+ type: string;
4
+ skill: string;
5
+ confidence: number;
6
+ evidence: string;
7
+ category?: string;
8
+ };
9
+ export declare function extractCorrectionPatterns(stats: SessionStats): Signal[];
10
+ export declare function extractToolFrequency(stats: SessionStats): Signal[];
11
+ export declare function extractDecisionLanguage(stats: SessionStats): Signal[];
12
+ export declare function extractDomainClusters(stats: SessionStats): Signal[];
13
+ export declare function extractPromptStructure(stats: SessionStats): Signal[];
14
+ export declare const ALL_EXTRACTORS: readonly [typeof extractCorrectionPatterns, typeof extractToolFrequency, typeof extractDecisionLanguage, typeof extractDomainClusters, typeof extractPromptStructure];
15
+ export declare function extractAllSignals(stats: SessionStats): Signal[];
@@ -0,0 +1,341 @@
1
+ // ---------- 1. Correction Patterns ----------
2
+ const CORRECTION_PATTERNS = [
3
+ // French
4
+ /\bnon\b/i,
5
+ /\bpas comme [cç]a\b/i,
6
+ /\bc'est pas\b/i,
7
+ /\bplut[oô]t\b/i,
8
+ /\brevert\b/i,
9
+ /\bannule\b/i,
10
+ /\brefais\b/i,
11
+ /\bmauvais\b/i,
12
+ /\bincorrect\b/i,
13
+ // English
14
+ /\bwrong\b/i,
15
+ /\bnot like that\b/i,
16
+ /\bno[,.]?\s/i,
17
+ /\bundo\b/i,
18
+ /\bdon't\b/i,
19
+ /\bshouldn't\b/i,
20
+ /\bactually\b/i,
21
+ /\binstead\b/i,
22
+ ];
23
+ export function extractCorrectionPatterns(stats) {
24
+ const signals = [];
25
+ const { messages } = stats;
26
+ for (let i = 1; i < messages.length; i++) {
27
+ const msg = messages[i];
28
+ if (msg.role !== "user")
29
+ continue;
30
+ // Check if previous message was from assistant
31
+ const prev = messages[i - 1];
32
+ if (!prev || prev.role !== "assistant")
33
+ continue;
34
+ const userText = getTextContent(msg);
35
+ if (!userText)
36
+ continue;
37
+ for (const pattern of CORRECTION_PATTERNS) {
38
+ if (pattern.test(userText)) {
39
+ // Extract what the assistant was doing from the previous message
40
+ const assistantContext = getTextContent(prev);
41
+ const toolNames = getToolNames(prev);
42
+ const context = toolNames.length > 0
43
+ ? `tools: ${toolNames.join(", ")}`
44
+ : truncate(assistantContext, 100);
45
+ signals.push({
46
+ type: "correction",
47
+ skill: inferSkillFromCorrection(userText, context),
48
+ confidence: 0.6,
49
+ evidence: `User correction: "${truncate(userText, 120)}" (after ${context})`,
50
+ category: "judgment",
51
+ });
52
+ break; // One signal per message
53
+ }
54
+ }
55
+ }
56
+ return signals;
57
+ }
58
+ function inferSkillFromCorrection(userText, context) {
59
+ const text = `${userText} ${context}`.toLowerCase();
60
+ if (/git|commit|branch|push|merge/.test(text))
61
+ return "version control judgment";
62
+ if (/test|spec|assert/.test(text))
63
+ return "testing methodology";
64
+ if (/css|style|layout|design/.test(text))
65
+ return "UI/UX design sense";
66
+ if (/type|interface|schema/.test(text))
67
+ return "type system design";
68
+ if (/security|auth|token/.test(text))
69
+ return "security awareness";
70
+ if (/perf|optim|speed|slow/.test(text))
71
+ return "performance optimization";
72
+ if (/api|route|endpoint/.test(text))
73
+ return "API design";
74
+ if (/archi|struct|pattern/.test(text))
75
+ return "software architecture";
76
+ return "code review judgment";
77
+ }
78
+ // ---------- 2. Tool Frequency ----------
79
+ const TOOL_CLUSTERS = {
80
+ Grep: { skill: "code archaeology", category: "tool_proficiency" },
81
+ Glob: { skill: "codebase navigation", category: "tool_proficiency" },
82
+ Read: { skill: "code comprehension", category: "hard_skill" },
83
+ Edit: { skill: "code editing precision", category: "hard_skill" },
84
+ Write: { skill: "code generation", category: "hard_skill" },
85
+ Bash: { skill: "shell scripting", category: "tool_proficiency" },
86
+ Agent: { skill: "task delegation", category: "methodology" },
87
+ WebSearch: { skill: "research methodology", category: "methodology" },
88
+ WebFetch: { skill: "API integration", category: "hard_skill" },
89
+ };
90
+ export function extractToolFrequency(stats) {
91
+ const signals = [];
92
+ const counts = new Map();
93
+ for (const tc of stats.toolCalls) {
94
+ counts.set(tc.name, (counts.get(tc.name) ?? 0) + 1);
95
+ }
96
+ const total = stats.toolCalls.length;
97
+ if (total === 0)
98
+ return signals;
99
+ // Also detect git usage in Bash commands
100
+ let gitCount = 0;
101
+ for (const tc of stats.toolCalls) {
102
+ if (tc.name === "Bash") {
103
+ const cmd = tc.input?.command;
104
+ if (typeof cmd === "string" && /\bgit\b/.test(cmd)) {
105
+ gitCount++;
106
+ }
107
+ }
108
+ }
109
+ if (gitCount > 3) {
110
+ const freq = gitCount / total;
111
+ signals.push({
112
+ type: "tool_frequency",
113
+ skill: "version control mastery",
114
+ confidence: Math.min(0.5 + freq * 2, 0.95),
115
+ evidence: `${gitCount} git commands out of ${total} total tool calls (${(freq * 100).toFixed(1)}%)`,
116
+ category: "tool_proficiency",
117
+ });
118
+ }
119
+ for (const [tool, info] of Object.entries(TOOL_CLUSTERS)) {
120
+ const count = counts.get(tool) ?? 0;
121
+ if (count < 3)
122
+ continue;
123
+ const freq = count / total;
124
+ signals.push({
125
+ type: "tool_frequency",
126
+ skill: info.skill,
127
+ confidence: Math.min(0.4 + freq * 2, 0.9),
128
+ evidence: `${tool} used ${count}/${total} times (${(freq * 100).toFixed(1)}%)`,
129
+ category: info.category,
130
+ });
131
+ }
132
+ return signals;
133
+ }
134
+ // ---------- 3. Decision Language ----------
135
+ const DECISION_PATTERNS = [
136
+ // French
137
+ { pattern: /je pr[eé]f[eè]re\s+(.{10,80})/i, mapTo: "preference" },
138
+ { pattern: /on va plut[oô]t\s+(.{10,80})/i, mapTo: "preference" },
139
+ { pattern: /toujours\s+(.{10,80})/i, mapTo: "always" },
140
+ { pattern: /jamais\s+(.{10,80})/i, mapTo: "never" },
141
+ { pattern: /il faut\s+(.{10,80})/i, mapTo: "always" },
142
+ { pattern: /on ne fait pas\s+(.{10,80})/i, mapTo: "never" },
143
+ // English
144
+ { pattern: /I prefer\s+(.{10,80})/i, mapTo: "preference" },
145
+ { pattern: /always\s+(.{10,80})/i, mapTo: "always" },
146
+ { pattern: /never\s+(.{10,80})/i, mapTo: "never" },
147
+ { pattern: /we should\s+(.{10,80})/i, mapTo: "preference" },
148
+ { pattern: /don'?t ever\s+(.{10,80})/i, mapTo: "never" },
149
+ { pattern: /make sure to\s+(.{10,80})/i, mapTo: "always" },
150
+ ];
151
+ export function extractDecisionLanguage(stats) {
152
+ const signals = [];
153
+ for (const msg of stats.messages) {
154
+ if (msg.role !== "user")
155
+ continue;
156
+ const text = getTextContent(msg);
157
+ if (!text)
158
+ continue;
159
+ for (const { pattern, mapTo } of DECISION_PATTERNS) {
160
+ const match = pattern.exec(text);
161
+ if (!match)
162
+ continue;
163
+ const criterion = match[1].trim().replace(/[.!,;]+$/, "");
164
+ const skill = inferSkillFromDecision(criterion);
165
+ signals.push({
166
+ type: "decision_language",
167
+ skill,
168
+ confidence: mapTo === "always" || mapTo === "never" ? 0.7 : 0.5,
169
+ evidence: `${mapTo === "never" ? "Anti-pattern" : "Decision criterion"}: "${truncate(match[0], 100)}"`,
170
+ category: "judgment",
171
+ });
172
+ }
173
+ }
174
+ return signals;
175
+ }
176
+ function inferSkillFromDecision(text) {
177
+ const lower = text.toLowerCase();
178
+ if (/tab|indent|format|lint|biome/.test(lower))
179
+ return "code style standards";
180
+ if (/test|coverage|spec/.test(lower))
181
+ return "testing methodology";
182
+ if (/secur|auth|token|secret|\.env|credential/.test(lower))
183
+ return "security practices";
184
+ if (/commit|branch|pr|merge/.test(lower))
185
+ return "version control workflow";
186
+ if (/type|interface|generic/.test(lower))
187
+ return "type system design";
188
+ if (/error|catch|throw|handle/.test(lower))
189
+ return "error handling strategy";
190
+ if (/component|render|ui/.test(lower))
191
+ return "component architecture";
192
+ if (/api|route|endpoint|rest/.test(lower))
193
+ return "API design";
194
+ if (/perf|cache|optim/.test(lower))
195
+ return "performance optimization";
196
+ return "engineering judgment";
197
+ }
198
+ // ---------- 4. Domain Clustering ----------
199
+ const DOMAIN_MAP = {
200
+ "lib/a2a": { skill: "agent-to-agent protocol", category: "domain_knowledge" },
201
+ "lib/ai": { skill: "AI/LLM integration", category: "domain_knowledge" },
202
+ "lib/competency": {
203
+ skill: "competency modeling",
204
+ category: "domain_knowledge",
205
+ },
206
+ "lib/db": { skill: "database design", category: "hard_skill" },
207
+ "lib/generative-ui": {
208
+ skill: "generative UI systems",
209
+ category: "hard_skill",
210
+ },
211
+ "components/": {
212
+ skill: "React component development",
213
+ category: "hard_skill",
214
+ },
215
+ "relay-worker": {
216
+ skill: "Cloudflare Workers development",
217
+ category: "hard_skill",
218
+ },
219
+ "app/api": { skill: "Next.js API routes", category: "hard_skill" },
220
+ "app/(chat)": {
221
+ skill: "chat application architecture",
222
+ category: "domain_knowledge",
223
+ },
224
+ scripts: {
225
+ skill: "build tooling and automation",
226
+ category: "tool_proficiency",
227
+ },
228
+ ".github": { skill: "CI/CD pipeline design", category: "methodology" },
229
+ content: { skill: "content strategy", category: "domain_knowledge" },
230
+ };
231
+ export function extractDomainClusters(stats) {
232
+ const signals = [];
233
+ const domainCounts = new Map();
234
+ for (const fp of stats.filesTouched) {
235
+ for (const [prefix, _info] of Object.entries(DOMAIN_MAP)) {
236
+ if (fp.includes(prefix)) {
237
+ domainCounts.set(prefix, (domainCounts.get(prefix) ?? 0) + 1);
238
+ break;
239
+ }
240
+ }
241
+ }
242
+ const totalFiles = stats.filesTouched.length;
243
+ if (totalFiles === 0)
244
+ return signals;
245
+ for (const [prefix, count] of domainCounts) {
246
+ if (count < 2)
247
+ continue;
248
+ const info = DOMAIN_MAP[prefix];
249
+ const freq = count / totalFiles;
250
+ signals.push({
251
+ type: "domain_cluster",
252
+ skill: info.skill,
253
+ confidence: Math.min(0.4 + freq * 1.5, 0.85),
254
+ evidence: `${count} files touched in ${prefix} (${(freq * 100).toFixed(1)}% of ${totalFiles} files)`,
255
+ category: info.category,
256
+ });
257
+ }
258
+ return signals;
259
+ }
260
+ // ---------- 5. Prompt Structure ----------
261
+ export function extractPromptStructure(stats) {
262
+ const signals = [];
263
+ let longStructuredCount = 0;
264
+ let shortImperativeCount = 0;
265
+ let totalUserMessages = 0;
266
+ for (const msg of stats.messages) {
267
+ if (msg.role !== "user")
268
+ continue;
269
+ const text = getTextContent(msg);
270
+ if (!text)
271
+ continue;
272
+ // Skip tool results (user messages that are just tool output)
273
+ if (msg.content.some((b) => b.type === "tool_result"))
274
+ continue;
275
+ totalUserMessages++;
276
+ const len = text.length;
277
+ // Long numbered prompts = spec methodology
278
+ if (len > 500 && /(?:\d+[.)]\s|\n[-*]\s)/.test(text)) {
279
+ longStructuredCount++;
280
+ }
281
+ // Short imperatives = rapid iteration
282
+ else if (len < 100) {
283
+ shortImperativeCount++;
284
+ }
285
+ }
286
+ if (totalUserMessages === 0)
287
+ return signals;
288
+ if (longStructuredCount >= 2) {
289
+ const freq = longStructuredCount / totalUserMessages;
290
+ signals.push({
291
+ type: "prompt_structure",
292
+ skill: "specification methodology",
293
+ confidence: Math.min(0.5 + freq * 2, 0.9),
294
+ evidence: `${longStructuredCount}/${totalUserMessages} user messages are long structured specs (>500 chars with numbered lists)`,
295
+ category: "methodology",
296
+ });
297
+ }
298
+ if (shortImperativeCount >= 5) {
299
+ const freq = shortImperativeCount / totalUserMessages;
300
+ signals.push({
301
+ type: "prompt_structure",
302
+ skill: "rapid iteration workflow",
303
+ confidence: Math.min(0.4 + freq, 0.85),
304
+ evidence: `${shortImperativeCount}/${totalUserMessages} user messages are short imperatives (<100 chars)`,
305
+ category: "methodology",
306
+ });
307
+ }
308
+ return signals;
309
+ }
310
+ // ---------- All extractors ----------
311
+ export const ALL_EXTRACTORS = [
312
+ extractCorrectionPatterns,
313
+ extractToolFrequency,
314
+ extractDecisionLanguage,
315
+ extractDomainClusters,
316
+ extractPromptStructure,
317
+ ];
318
+ export function extractAllSignals(stats) {
319
+ const signals = [];
320
+ for (const extractor of ALL_EXTRACTORS) {
321
+ signals.push(...extractor(stats));
322
+ }
323
+ return signals;
324
+ }
325
+ // ---------- Util ----------
326
+ function getTextContent(msg) {
327
+ return msg.content
328
+ .filter((b) => b.type === "text")
329
+ .map((b) => b.text)
330
+ .join("\n");
331
+ }
332
+ function getToolNames(msg) {
333
+ return msg.content
334
+ .filter((b) => b.type === "tool_use")
335
+ .map((b) => b.name);
336
+ }
337
+ function truncate(s, max) {
338
+ if (s.length <= max)
339
+ return s;
340
+ return `${s.slice(0, max)}...`;
341
+ }
@@ -0,0 +1,13 @@
1
+ type JsonObject = Record<string, unknown>;
2
+ /** Resolve the absolute path for the temet binary at install time. */
3
+ export declare function resolveTemetBinary(): string | null;
4
+ export declare function readSettings(settingsPath: string): JsonObject;
5
+ export declare function writeSettings(settingsPath: string, settings: JsonObject): void;
6
+ /** Check if a temet SessionEnd hook is already installed. */
7
+ export declare function isHookInstalled(settings: JsonObject): boolean;
8
+ /** Add the SessionEnd hook entry. Idempotent. */
9
+ export declare function installHook(settings: JsonObject, binaryCommand: string): JsonObject;
10
+ /** Remove the temet SessionEnd hook entry. */
11
+ export declare function uninstallHook(settings: JsonObject): JsonObject;
12
+ export declare function getSettingsPath(): string;
13
+ export {};