@xuda.io/ai_module 1.1.5638 → 1.1.5639

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.
package/index.mjs CHANGED
@@ -4738,6 +4738,154 @@ export const diagnose_vps_snapshot = async function (req) {
4738
4738
  }
4739
4739
  };
4740
4740
 
4741
+ // Map a studio program's menuType to a friendly, user-facing noun so the
4742
+ // release-notes prompt talks about "screens" and "workflows" rather than the
4743
+ // internal type ids. Unknown types fall back to a de-underscored label.
4744
+ const _human_menu_type = function (t) {
4745
+ const map = {
4746
+ component: 'screen',
4747
+ workflow: 'workflow',
4748
+ table: 'data table',
4749
+ javascript: 'logic',
4750
+ js: 'logic',
4751
+ ai_agent: 'AI agent',
4752
+ api: 'API',
4753
+ style: 'style',
4754
+ function: 'function',
4755
+ query: 'query',
4756
+ report: 'report',
4757
+ };
4758
+ return map[t] || (t ? String(t).replace(/_/g, ' ') : 'item');
4759
+ };
4760
+
4761
+ // Generate user-facing release notes for a build the developer is about to
4762
+ // create. READ-ONLY: reads the project's PUBLISHED studio programs, diffs them
4763
+ // against the previous release's timestamp to see what changed since the last
4764
+ // build, and asks the model for a concise, plain-language changelog. Never
4765
+ // writes the release or mutates a program — the notes are returned for the
4766
+ // developer to review/edit before they hit Build. Credits are metered to the
4767
+ // caller via submit_chat_gpt_prompt -> record_ai_usage.
4768
+ // req: { app_id, version?, build_type?(major|minor|patch), instructions? }
4769
+ // -> { code:1, data:{ notes, changed_count, since_build, version } }
4770
+ // { code:-6, insufficient_credits:true } when the account is out of credits.
4771
+ export const generate_release_notes = async function (req) {
4772
+ const uid = req.uid || req.token_ret?.data?.uid;
4773
+ const app_id_in = req.app_id;
4774
+ const version = String(req.version || '').trim();
4775
+ const build_type = String(req.build_type || '').trim(); // major | minor | patch
4776
+ const steer = String(req.instructions || '')
4777
+ .trim()
4778
+ .slice(0, 400); // optional free-text hint from the developer
4779
+
4780
+ if (!uid) return { code: -1, data: 'not authorized' };
4781
+ if (!app_id_in) return { code: -1, data: 'app_id required' };
4782
+
4783
+ // Credit gate — usage is metered, but block up-front if the account is
4784
+ // already out (validate_credits_limit RETURNS the error, never throws).
4785
+ const credit_err = await validate_credits_limit(uid, null, _conf.release_notes?.model || _conf.default_ai_model, 'release notes');
4786
+ if (credit_err) return { code: -6, data: credit_err.message || 'insufficient credits', insufficient_credits: true };
4787
+
4788
+ // Resolve to the project (master) app — build/release data lives there.
4789
+ let app_obj;
4790
+ let app_ref;
4791
+ try {
4792
+ app_ref = await _common.get_project_app_id(app_id_in, true);
4793
+ const app_ret = await db_module.get_app_obj(app_ref);
4794
+ if (app_ret.code < 0 || !app_ret.data) return { code: -1, data: 'app not found' };
4795
+ app_obj = app_ret.data;
4796
+ } catch (e) {
4797
+ return { code: -1, data: 'app not found' };
4798
+ }
4799
+
4800
+ const app_name = app_obj.app_name || 'the app';
4801
+ const prev_version = app_obj.app_publish_info?.version || '';
4802
+ const last_build = app_obj.app_publish_info?.app_lastBuild || 0;
4803
+
4804
+ // Cutoff = the previous release's timestamp. Any program whose `ts`
4805
+ // (last-saved time) is newer than this changed since that build.
4806
+ let since_ts = 0;
4807
+ try {
4808
+ const rel = await db_module.find_app_couch_query(app_ref, {
4809
+ selector: { docType: 'release', stat: { $lt: 4 } },
4810
+ sort: [{ build_id: 'desc' }],
4811
+ limit: 1,
4812
+ });
4813
+ since_ts = rel.docs?.[0]?.date || 0;
4814
+ } catch (e) {
4815
+ since_ts = 0;
4816
+ }
4817
+
4818
+ // Published programs, projected to a tiny summary (bounds prompt tokens).
4819
+ let progs = [];
4820
+ try {
4821
+ const res = await db_module.find_app_couch_query(app_ref, {
4822
+ selector: { docType: 'studio', stat: 3 },
4823
+ fields: ['ts', 'properties.menuType', 'properties.menuName', 'properties.menuTitle'],
4824
+ limit: 99999,
4825
+ });
4826
+ progs = res.docs || [];
4827
+ } catch (e) {
4828
+ progs = [];
4829
+ }
4830
+
4831
+ const first_build = !since_ts || last_build === 0;
4832
+ const changed = progs.filter((p) => p && (first_build || (p.ts && p.ts > since_ts))).sort((a, b) => (b.ts || 0) - (a.ts || 0));
4833
+
4834
+ // Compact, deduped change list for the prompt (name + type; capped at 80).
4835
+ const seen = new Set();
4836
+ const items = [];
4837
+ for (const p of changed) {
4838
+ const name = p.properties?.menuTitle || p.properties?.menuName || 'Untitled';
4839
+ const type = _human_menu_type(p.properties?.menuType);
4840
+ const key = `${type}:${name}`;
4841
+ if (seen.has(key)) continue;
4842
+ seen.add(key);
4843
+ items.push({ name, type });
4844
+ if (items.length >= 80) break;
4845
+ }
4846
+
4847
+ const by_type = {};
4848
+ for (const it of items) by_type[it.type] = (by_type[it.type] || 0) + 1;
4849
+ const type_summary = Object.entries(by_type)
4850
+ .map(([t, n]) => `${n} ${t}${n > 1 ? 's' : ''}`)
4851
+ .join(', ');
4852
+
4853
+ const parts = [
4854
+ `You are writing end-user release notes for a no-code app called "${app_name}" built on the Xuda platform.`,
4855
+ `The developer is publishing a new build to every deployment of this app. Write short, friendly, user-facing release notes that tell the people who USE the app what is new — in plain language, not technical jargon.`,
4856
+ '',
4857
+ 'RULES:',
4858
+ '- Output ONLY 2 to 5 concise bullet points, each a single short line. No title, no preamble, no sign-off — bullets only.',
4859
+ '- Describe outcomes and improvements ("New School Info screen", "Faster class enrollment"), never internal file names or program types.',
4860
+ '- Group related changes into one bullet instead of listing every item.',
4861
+ '- Neutral, professional tone. Never invent features that the change list does not imply.',
4862
+ '- Write in the SAME language as the program names below (e.g. Hebrew names -> Hebrew notes).',
4863
+ steer ? `- The developer added this guidance — follow it: ${steer}` : '',
4864
+ '',
4865
+ `Version: ${prev_version ? prev_version + ' -> ' : ''}${version || '(new)'}${build_type ? ` (${build_type} release)` : ''}`,
4866
+ first_build ? 'This is the FIRST published build. Write a brief "initial release" style note highlighting the main areas of the app.' : `Programs changed since the last build (${type_summary || 'general updates'}):`,
4867
+ items.length ? items.map((it) => `- ${it.name} (${it.type})`).join('\n') : '(No specific program changes were detected — write a brief note about minor improvements and stability fixes.)',
4868
+ ].filter(Boolean);
4869
+
4870
+ // account_profile_info is REQUIRED for usage to be metered: record_ai_usage
4871
+ // throws (and skips broadcast_credits, so the nav meter never moves) without
4872
+ // it. Fetch it like classify_external_app_scan / the chat flows do.
4873
+ const account_profile_info = await get_active_account_profile_info(uid);
4874
+
4875
+ const ret = await submit_chat_gpt_prompt({
4876
+ uid,
4877
+ prompt: parts.join('\n'),
4878
+ model: _conf.release_notes?.model || _conf.default_ai_model,
4879
+ metadata: { func: 'generate_release_notes', app_id: app_ref, version },
4880
+ account_profile_info,
4881
+ });
4882
+ if (ret.code < 0) return { code: -1, data: ret.data };
4883
+
4884
+ const notes = String(ret.data || '').trim();
4885
+ if (!notes) return { code: -1, data: 'no notes generated' };
4886
+ return { code: 1, data: { notes, changed_count: changed.length, since_build: last_build, version } };
4887
+ };
4888
+
4741
4889
  function getFirstNWords(text, n = 10) {
4742
4890
  // Split on whitespace, keep punctuation attached to words
4743
4891
  const words = text.match(/\S+/g) || [];
package/index_ms.mjs CHANGED
@@ -133,6 +133,10 @@ export const diagnose_vps_snapshot = async function (...args) {
133
133
  return await broker.send_to_queue("diagnose_vps_snapshot", ...args);
134
134
  };
135
135
 
136
+ export const generate_release_notes = async function (...args) {
137
+ return await broker.send_to_queue("generate_release_notes", ...args);
138
+ };
139
+
136
140
  export const create_openai_conversation = async function (...args) {
137
141
  return await broker.send_to_queue("create_openai_conversation", ...args);
138
142
  };
package/index_msa.mjs CHANGED
@@ -133,6 +133,10 @@ export const diagnose_vps_snapshot = function (...args) {
133
133
  broker.send_to_queue_async("diagnose_vps_snapshot", ...args);
134
134
  };
135
135
 
136
+ export const generate_release_notes = function (...args) {
137
+ broker.send_to_queue_async("generate_release_notes", ...args);
138
+ };
139
+
136
140
  export const create_openai_conversation = function (...args) {
137
141
  broker.send_to_queue_async("create_openai_conversation", ...args);
138
142
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/ai_module",
3
- "version": "1.1.5638",
3
+ "version": "1.1.5639",
4
4
  "description": "Xuda AI Module",
5
5
  "main": "index.mjs",
6
6
  "type": "module",