Skill Tracer
Skills › gstack › gstack › declared-annotation.ts

declared-annotation.ts

121 lines · scripts/declared-annotation.ts at 3f79d81

1/**
2 * Declared-profile annotation helper (plan-tune cathedral T7).
3 *
4 * Given a kebab signal_key from scripts/question-registry.ts, returns a
5 * one-line plain-English annotation when the user's declared profile is in
6 * a strong band on the matching dimension, else null. Read-only — never
7 * mutates the profile.
8 *
9 * Signature uses kebab signal_key per D2/Codex correction. Internally maps
10 * to the underscore Dimension key by consulting SIGNAL_MAP and picking the
11 * dimension this signal influences most strongly.
12 *
13 * Used by:
14 * - hosts/claude/hooks/question-preference-hook (Layer 3 injection path,
15 * when AUQ mutation lands)
16 * - scripts/resolvers/question-tuning.ts preamble (Layer 9 fallback,
17 * host-portable path on Codex / older Claude Code)
18 *
19 * NOT used for AUTO_DECIDE. Annotation is advisory only — declared-only
20 * per TODOS.md E1 substrate-risk guidance. Inferred-driven AUTO_DECIDE
21 * remains v2.
22 */
23import * as fs from 'fs';
24import * as path from 'path';
26import { SIGNAL_MAP, type Dimension, ALL_DIMENSIONS } from './psychographic-signals';
27import { resolveStateRoot } from '../lib/state-root';
29const STRONG_HIGH = 0.7;
30const STRONG_LOW = 0.3;
32/**
33 * Plain-English phrasing per dimension + band. Keep one sentence each.
34 * Used directly in question prose, so phrasing matters.
35 */
36const DIMENSION_PHRASING: Record<Dimension, { high: string; low: string }> = {
37 scope_appetite: {
38 high: 'Your declared profile leans complete-implementation (boil the ocean).',
39 low: 'Your declared profile leans ship-small-fast.',
40 },
41 risk_tolerance: {
42 high: 'Your declared profile leans move-fast.',
43 low: 'Your declared profile leans check-carefully.',
44 },
45 detail_preference: {
46 high: 'Your declared profile leans verbose-with-tradeoffs.',
47 low: 'Your declared profile leans terse, just-do-it.',
48 },
49 autonomy: {
50 high: 'Your declared profile leans delegate-and-trust.',
51 low: 'Your declared profile leans consult-me-first.',
52 },
53 architecture_care: {
54 high: 'Your declared profile leans get-the-design-right.',
55 low: 'Your declared profile leans pragmatic-ship-it.',
56 },
57};
59interface DeveloperProfile {
60 declared?: Partial<Record<Dimension, number>>;
63function stateRoot(): string {
64 return resolveStateRoot();
67function readProfile(): DeveloperProfile | null {
68 try {
69 const p = path.join(stateRoot(), 'developer-profile.json');
70 if (!fs.existsSync(p)) return null;
71 return JSON.parse(fs.readFileSync(p, 'utf-8'));
72 } catch {
73 return null;
74 }
77/**
78 * Determine which dimension a signal_key influences most strongly.
79 * Sums |delta| across all user_choice → DimensionDelta[] entries for that
80 * signal, returns the dimension with the largest total influence.
81 * Returns null if the signal_key isn't in the map.
82 */
83export function primaryDimensionFor(signalKey: string): Dimension | null {
84 const entry = SIGNAL_MAP[signalKey];
85 if (!entry) return null;
86 const totals: Partial<Record<Dimension, number>> = {};
87 for (const choice of Object.keys(entry)) {
88 for (const dd of entry[choice]) {
89 totals[dd.dim] = (totals[dd.dim] ?? 0) + Math.abs(dd.delta);
90 }
91 }
92 let best: Dimension | null = null;
93 let bestVal = -Infinity;
94 for (const d of ALL_DIMENSIONS) {
95 const v = totals[d] ?? 0;
96 if (v > bestVal) {
97 bestVal = v;
98 best = d;
99 }
100 }
101 return bestVal > 0 ? best : null;
104/**
105 * Given a signal_key, return a one-line plain-English annotation when
106 * the user's declared profile is in a strong band on the primary dim,
107 * else null.
108 */
109export function getDeclaredAnnotation(signalKey: string): string | null {
110 if (!signalKey || typeof signalKey !== 'string') return null;
111 const dim = primaryDimensionFor(signalKey);
112 if (!dim) return null;
114 const profile = readProfile();
115 const declared = profile?.declared?.[dim];
116 if (typeof declared !== 'number') return null;
118 if (declared >= STRONG_HIGH) return DIMENSION_PHRASING[dim].high;
119 if (declared <= STRONG_LOW) return DIMENSION_PHRASING[dim].low;
120 return null;