mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-04 15:57:33 +01:00
Bins, careful/freeze hooks, setup, upgrade migrations, browse/src, design, ios-qa daemon, lib and scripts resolve the state root through bin/gstack-state-root.sh (bash) or lib/state-root.ts (TS). Bins source the twin and stop with a reinstall message when it is missing; hooks source it and never spawn gstack-paths. browse/src/config.ts and lib/cso/state.ts delegate to resolveStateRoot. Analytics writers and readers move together so the usage log stays one file. gstack-uninstall deletes state only at ~/.gstack, refuses (exit 2) when it resolves to /, $HOME or an ancestor, the checkout or the git root, and leaves any other resolved root in place with the removal command. Fixtures that copy single bins now copy the twin.
122 lines
4.0 KiB
TypeScript
122 lines
4.0 KiB
TypeScript
/**
|
|
* Declared-profile annotation helper (plan-tune cathedral T7).
|
|
*
|
|
* Given a kebab signal_key from scripts/question-registry.ts, returns a
|
|
* one-line plain-English annotation when the user's declared profile is in
|
|
* a strong band on the matching dimension, else null. Read-only — never
|
|
* mutates the profile.
|
|
*
|
|
* Signature uses kebab signal_key per D2/Codex correction. Internally maps
|
|
* to the underscore Dimension key by consulting SIGNAL_MAP and picking the
|
|
* dimension this signal influences most strongly.
|
|
*
|
|
* Used by:
|
|
* - hosts/claude/hooks/question-preference-hook (Layer 3 injection path,
|
|
* when AUQ mutation lands)
|
|
* - scripts/resolvers/question-tuning.ts preamble (Layer 9 fallback,
|
|
* host-portable path on Codex / older Claude Code)
|
|
*
|
|
* NOT used for AUTO_DECIDE. Annotation is advisory only — declared-only
|
|
* per TODOS.md E1 substrate-risk guidance. Inferred-driven AUTO_DECIDE
|
|
* remains v2.
|
|
*/
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
import { SIGNAL_MAP, type Dimension, ALL_DIMENSIONS } from './psychographic-signals';
|
|
import { resolveStateRoot } from '../lib/state-root';
|
|
|
|
const STRONG_HIGH = 0.7;
|
|
const STRONG_LOW = 0.3;
|
|
|
|
/**
|
|
* Plain-English phrasing per dimension + band. Keep one sentence each.
|
|
* Used directly in question prose, so phrasing matters.
|
|
*/
|
|
const DIMENSION_PHRASING: Record<Dimension, { high: string; low: string }> = {
|
|
scope_appetite: {
|
|
high: 'Your declared profile leans complete-implementation (boil the ocean).',
|
|
low: 'Your declared profile leans ship-small-fast.',
|
|
},
|
|
risk_tolerance: {
|
|
high: 'Your declared profile leans move-fast.',
|
|
low: 'Your declared profile leans check-carefully.',
|
|
},
|
|
detail_preference: {
|
|
high: 'Your declared profile leans verbose-with-tradeoffs.',
|
|
low: 'Your declared profile leans terse, just-do-it.',
|
|
},
|
|
autonomy: {
|
|
high: 'Your declared profile leans delegate-and-trust.',
|
|
low: 'Your declared profile leans consult-me-first.',
|
|
},
|
|
architecture_care: {
|
|
high: 'Your declared profile leans get-the-design-right.',
|
|
low: 'Your declared profile leans pragmatic-ship-it.',
|
|
},
|
|
};
|
|
|
|
interface DeveloperProfile {
|
|
declared?: Partial<Record<Dimension, number>>;
|
|
}
|
|
|
|
function stateRoot(): string {
|
|
return resolveStateRoot();
|
|
}
|
|
|
|
function readProfile(): DeveloperProfile | null {
|
|
try {
|
|
const p = path.join(stateRoot(), 'developer-profile.json');
|
|
if (!fs.existsSync(p)) return null;
|
|
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine which dimension a signal_key influences most strongly.
|
|
* Sums |delta| across all user_choice → DimensionDelta[] entries for that
|
|
* signal, returns the dimension with the largest total influence.
|
|
* Returns null if the signal_key isn't in the map.
|
|
*/
|
|
export function primaryDimensionFor(signalKey: string): Dimension | null {
|
|
const entry = SIGNAL_MAP[signalKey];
|
|
if (!entry) return null;
|
|
const totals: Partial<Record<Dimension, number>> = {};
|
|
for (const choice of Object.keys(entry)) {
|
|
for (const dd of entry[choice]) {
|
|
totals[dd.dim] = (totals[dd.dim] ?? 0) + Math.abs(dd.delta);
|
|
}
|
|
}
|
|
let best: Dimension | null = null;
|
|
let bestVal = -Infinity;
|
|
for (const d of ALL_DIMENSIONS) {
|
|
const v = totals[d] ?? 0;
|
|
if (v > bestVal) {
|
|
bestVal = v;
|
|
best = d;
|
|
}
|
|
}
|
|
return bestVal > 0 ? best : null;
|
|
}
|
|
|
|
/**
|
|
* Given a signal_key, return a one-line plain-English annotation when
|
|
* the user's declared profile is in a strong band on the primary dim,
|
|
* else null.
|
|
*/
|
|
export function getDeclaredAnnotation(signalKey: string): string | null {
|
|
if (!signalKey || typeof signalKey !== 'string') return null;
|
|
const dim = primaryDimensionFor(signalKey);
|
|
if (!dim) return null;
|
|
|
|
const profile = readProfile();
|
|
const declared = profile?.declared?.[dim];
|
|
if (typeof declared !== 'number') return null;
|
|
|
|
if (declared >= STRONG_HIGH) return DIMENSION_PHRASING[dim].high;
|
|
if (declared <= STRONG_LOW) return DIMENSION_PHRASING[dim].low;
|
|
return null;
|
|
}
|