Setting the file. One moment.
Prefs Store · Media Use · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page ⋯
scripts/11 files
131
function recordUserSighting
— line 131
This file
Number 27.24
Position 24 of 78
Type JavaScript
Size 6 KB
Lines 180 scripts/lib/ prefs-store.mjs
JavaScript · 180 lines · 6 KB
12 * here only once the same value has been confirmed in two different projects
13 * (`PROMOTE_AT`), so a one-off choice never pollutes the global defaults.
14 * Pre-promotion evidence accumulates in the user file's `sightings` ledger —
15 * project files can't see each other, so the cross-project count has to live
16 * user-side.
17 *
18 * Consumption contract (brief-contract § 2, Remembered defaults): a remembered
19 * value becomes the recommended option with a receipt naming its source — it
20 * never skips a question, and explicit request content always wins.
21 */
22
23 const PREFS_FILE = "preferences.json" ;
24
25 /** Keys the brief contract records; `style_preset` is stored per workflow. */
26 export const PREFERENCE_KEYS = [
27 "destination" ,
28 "aspect" ,
29 "language" ,
30 "flow" ,
31 "storyboard" ,
32 "voice" ,
33 "style_preset" ,
34 ];
35
36 /** A value must be confirmed in this many distinct projects to go user-tier. */
37 export const PROMOTE_AT = 2 ;
38
39 export function projectPrefsPath ( projectDir ) {
40 return join ( resolve (projectDir), ".media" , PREFS_FILE );
41 }
42
43 export function userPrefsPath () {
44 return join ( homedir (), ".media" , PREFS_FILE );
45 }
46
47 function emptyFile () {
48 return { version: 1 , preferences: {}, sightings: {} };
49 }
50
51 function isRecord ( value ) {
52 return typeof value === "object" && value !== null && ! Array. isArray (value);
53 }
54
55 /** Tolerant read — a missing or malformed file counts as empty. */
56 function readPrefsFile ( path ) {
57 try {
58 if ( ! existsSync (path)) return emptyFile ();
59 const parsed = JSON . parse ( readFileSync (path, "utf8" ));
60 if ( ! isRecord (parsed)) return emptyFile ();
61 return {
62 version: 1 ,
63 preferences: isRecord (parsed.preferences) ? parsed.preferences : {},
64 sightings: isRecord (parsed.sightings) ? parsed.sightings : {},
65 };
66 } catch {
67 return emptyFile ();
68 }
69 }
70
71 /** Atomic write (tmp + rename) so a crash never leaves a torn file. */
72 function writePrefsFile ( path , file ) {
73 mkdirSync ( dirname (path), { recursive: true });
74 const tmp = `${ path }.tmp` ;
75 writeFileSync (tmp, `${ JSON . stringify ( file , null , 2 ) } \n ` );
76 renameSync (tmp, path);
77 }
78
79 /** `style_preset` entries are stored per workflow as `style_preset.<workflow>`. */
80 export function preferenceKeyFor ( key , workflow ) {
81 return key === "style_preset" && workflow ? `style_preset.${ workflow }` : key;
82 }
83
84 function validEntry ( entry ) {
85 return isRecord (entry) && typeof entry.value === "string" && entry.value. length > 0 ;
86 }
87
88 /**
89 * The merged view the brief reads: user-tier promoted entries first, project
90 * entries on top (project wins). Each entry carries `source` plus the receipt
91 * material (`confirmed_in`, `updated_at`).
92 */
93 export function mergedPreferences ( projectDir ) {
94 const user = readPrefsFile ( userPrefsPath ());
95 const project = readPrefsFile ( projectPrefsPath (projectDir));
96 const merged = {};
97 for ( const [ key , entry ] of Object. entries (user.preferences)) {
98 if ( validEntry (entry)) merged[key] = { ... entry, source: "user" };
99 }
100 for ( const [ key , entry ] of Object. entries (project.preferences)) {
101 if ( validEntry (entry)) merged[key] = { ... entry, source: "project" };
102 }
103 return merged;
104 }
105
106 function dedupe ( list ) {
107 return [ ...new Set (list)];
108 }
109
110 /**
111 * Project tier: same value accumulates confirmations; a changed value starts
112 * provenance over (the old confirmations vouched for the old value).
113 */
114 function recordProjectTier ( projectDir , fullKey , value , projectName , now ) {
115 const path = projectPrefsPath (projectDir);
116 const file = readPrefsFile (path);
117 const previous = file.preferences[fullKey];
118 const keepProvenance = validEntry (previous) && previous.value === value;
119 const confirmedIn = keepProvenance
120 ? dedupe ([ ... (Array. isArray (previous.confirmed_in) ? previous.confirmed_in : []), projectName])
121 : [projectName];
122 file.preferences[fullKey] = { value, confirmed_in: confirmedIn, updated_at: now };
123 writePrefsFile (path, file);
124 return confirmedIn;
125 }
126
127 /**
128 * User tier: accumulate this sighting in the ledger, and promote the key once
129 * the same value has been confirmed in PROMOTE_AT distinct projects.
130 */
131 function recordUserSighting ( fullKey , value , projectName , now ) {
132 const path = userPrefsPath ();
133 const file = readPrefsFile (path);
134 const keySightings = isRecord (file.sightings[fullKey]) ? file.sightings[fullKey] : {};
135 const seenIn = dedupe ([
136 ... (Array. isArray (keySightings[value]) ? keySightings[value] : []),
137 projectName,
138 ]);
139 keySightings[value] = seenIn;
140 file.sightings[fullKey] = keySightings;
141 const promoted = seenIn. length >= PROMOTE_AT ;
142 if (promoted) {
143 file.preferences[fullKey] = { value, confirmed_in: seenIn, updated_at: now };
144 }
145 writePrefsFile (path, file);
146 return promoted;
147 }
148
149 /**
150 * Record one confirmed brief answer. Always writes the project tier; feeds the
151 * user tier's sightings ledger and promotes once the same value has been
152 * confirmed in PROMOTE_AT distinct projects. Idempotent per project.
153 */
154 export function recordPreference ({ projectDir , key , value , workflow }) {
155 if ( ! PREFERENCE_KEYS . includes (key)) {
156 throw new Error ( `unknown preference key: "${ key }" (known: ${ PREFERENCE_KEYS . join ( ", " ) })` );
157 }
158 if ( typeof value !== "string" || ! value. trim ()) {
159 throw new Error ( "a preference needs a non-empty string value" );
160 }
161 if (key === "style_preset" && ( ! workflow || ! String (workflow). trim ())) {
162 throw new Error ( "style_preset is stored per workflow — pass --workflow <w>" );
163 }
164 const fullKey = preferenceKeyFor (key, workflow);
165 const projectName = basename ( resolve (projectDir));
166 const trimmed = value. trim ();
167 const now = new Date (). toISOString ();
168
169 const confirmedIn = recordProjectTier (projectDir, fullKey, trimmed, projectName, now);
170
171 // Best-effort — a read-only home directory must never fail a brief.
172 let promoted = false ;
173 try {
174 promoted = recordUserSighting (fullKey, trimmed, projectName, now);
175 } catch {
176 // The project record already landed; promotion just waits for next time.
177 }
178
179 return { key: fullKey, value: trimmed, confirmed_in: confirmedIn, promoted };
180 }