@yuneta/gobj-ui 7.23.194 → 7.23.196

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.
@@ -19,13 +19,15 @@
19
19
  * fkey, the order of a new sibling, the version that publishes
20
20
  * the change.
21
21
  *
22
- * THE VERSION IS THE POINT, not a detail. A topic whose columns
23
- * changed and whose `topic_version` did not is a topic whose
24
- * persisted `topic_cols.json` masks the whole edit: the restart
25
- * succeeds and nothing moved. Nothing in the store says so, and
26
- * the log that would is on the node. So every write here raises
27
- * the version of the topic it touched, and the toolbar says which
28
- * topics are waiting for a restart.
22
+ * AN EDIT IS A DRAFT. A write here moves no version and reaches no
23
+ * treedb: the host SAVES it (C_TREEDB's save-schema raises the
24
+ * topic_version of what changed and the schema_version, once) and
25
+ * APPLIES it (apply-schema + a restart of the owning yuno). Until
26
+ * the owner's design of M36 (2026-09-21 review) every write here
27
+ * raised both versions, so an edit half made was already the
28
+ * schema of the next start. The topic list marks what this session
29
+ * wrote and has not saved; the host's EV_REFRESH after a save is
30
+ * what forgets it.
29
31
  *
30
32
  * WHAT ELSE IS HERE, and why each is here and not somewhere else:
31
33
  *
@@ -572,21 +574,23 @@ function start_measuring(gobj)
572
574
  }
573
575
 
574
576
  /***************************************************************
575
- * "This topic was written and its version did not move."
577
+ * "This topic was written in this session and is not saved."
576
578
  *
577
- * ONE predicate, because it is asked in two places -- the topic
578
- * list draws a chip with it and the column view draws a banner --
579
- * and two copies of a rule is how they drift.
579
+ * An edit is a DRAFT: it moves no version and reaches no treedb
580
+ * until the host saves it (C_TREEDB's save-schema, which raises the
581
+ * versions of what changed) -- and the save makes the host send
582
+ * EV_REFRESH, whose reload is what forgets the drafts.
583
+ *
584
+ * ONE predicate, because it is asked in three places -- the topic
585
+ * list draws a chip with it, the column view a banner, the export
586
+ * a warning -- and copies of a rule are how they drift.
580
587
  ***************************************************************/
581
- function needs_version_bump(written, baseline, topic)
588
+ function topic_is_draft(written, topic)
582
589
  {
583
- if(!topic || !written || !baseline) {
584
- return false;
585
- }
586
- if(!written[topic.id]) {
590
+ if(!topic || !written) {
587
591
  return false;
588
592
  }
589
- return String(baseline[topic.id]) === String(topic.topic_version);
593
+ return written[topic.id]? true: false;
590
594
  }
591
595
 
592
596
  /***************************************************************
@@ -1041,7 +1045,7 @@ function render_topics(gobj, $body)
1041
1045
  let twice = repeated_names(treedb.topics);
1042
1046
  let $rows = [];
1043
1047
  for(let topic of treedb.topics) {
1044
- let pending = needs_version_bump(priv.written, priv.baseline, topic);
1048
+ let pending = topic_is_draft(priv.written, topic);
1045
1049
  /* Two topics of one name: the id is what tells them apart, and
1046
1050
  * it is what the url has to carry — addressing by name would
1047
1051
  * open the first one whichever row was clicked. */
@@ -1192,7 +1196,7 @@ function render_columns(gobj, $body)
1192
1196
  * The version banner: the one thing that makes
1193
1197
  * an edit succeed and change nothing.
1194
1198
  *----------------------------------------------*/
1195
- let $banner = version_banner(gobj, topic);
1199
+ let $banner = draft_banner(gobj, topic);
1196
1200
  if($banner) {
1197
1201
  $wrap.appendChild($banner);
1198
1202
  }
@@ -1235,48 +1239,34 @@ function render_columns(gobj, $body)
1235
1239
  }
1236
1240
 
1237
1241
  /***************************************************************
1238
- * "This topic changed and its version did not."
1242
+ * "This topic has changes that are not saved."
1239
1243
  *
1240
- * Only shown when it is TRUE — the topic was written in this
1241
- * session and its `topic_version` still reads what it read when
1242
- * the model loaded. Every write this view makes raises it, so
1243
- * the banner is for the case where something else did not: an
1244
- * edit made from the raw tables, or a bump undone by hand.
1244
+ * A draft, not an error: the edit is in __system__ and moves no
1245
+ * version, so the treedb does not see it until the host saves it
1246
+ * (and applies it). The banner says so and offers nothing -- the
1247
+ * save is the host's, one level up, because it publishes the whole
1248
+ * treedb and not one topic.
1245
1249
  ***************************************************************/
1246
- function version_banner(gobj, topic)
1250
+ function draft_banner(gobj, topic)
1247
1251
  {
1248
1252
  let priv = gobj.priv;
1249
1253
 
1250
- if(!needs_version_bump(priv.written, priv.baseline, topic)) {
1254
+ if(!topic_is_draft(priv.written, topic)) {
1251
1255
  return null;
1252
1256
  }
1253
1257
  if(is_readonly(gobj)) {
1254
1258
  return null;
1255
1259
  }
1256
1260
 
1257
- let $banner = createElement2(
1258
- ["div", {class: "SCHEMA_VERSION_BANNER notification is-warning is-light p-3 mb-3 " +
1261
+ return createElement2(
1262
+ ["div", {class: "SCHEMA_DRAFT_BANNER notification is-warning is-light p-3 mb-3 " +
1259
1263
  "is-flex is-align-items-center", style: "gap:.5rem;"}, [
1260
1264
  ["span", {class: "icon"}, [["i", {class: "yi-triangle-exclamation"}]]],
1261
- ["span", {class: "SCHEMA_VERSION_TEXT is-size-7", i18n: "version not raised"},
1262
- t("version not raised")],
1263
- ["button", {class: "SCHEMA_VERSION_BUMP button is-warning",
1264
- type: "button",
1265
- style: "margin-left:auto;",
1266
- title: t("raise the version"), "aria-label": t("raise the version"),
1267
- "data-i18n-title": "raise the version",
1268
- "data-i18n-aria-label": "raise the version"}, [
1269
- ["span", {class: "icon"}, [["i", {class: "yi-circle-up"}]]],
1270
- ["span", {i18n: "raise"}, t("raise")]
1271
- ]]
1265
+ ["span", {class: "SCHEMA_DRAFT_TEXT is-size-7",
1266
+ i18n: "unsaved schema changes: save to publish them"},
1267
+ t("unsaved schema changes: save to publish them")]
1272
1268
  ]]
1273
1269
  );
1274
- let $bump = $banner.querySelector(".SCHEMA_VERSION_BUMP");
1275
- $bump.addEventListener("click", (evt) => {
1276
- evt.stopPropagation();
1277
- gobj_send_event(gobj, "EV_BUMP_VERSION", {topic: topic.name}, gobj);
1278
- });
1279
- return $banner;
1280
1270
  }
1281
1271
 
1282
1272
  /***************************************************************
@@ -2001,6 +1991,8 @@ function open_report(gobj, findings, treedb_id)
2001
1991
  ***************************************************************/
2002
1992
  function open_export(gobj, treedb)
2003
1993
  {
1994
+ let priv = gobj.priv;
1995
+ let drafts = treedb.topics.some((topic) => topic_is_draft(priv.written, topic));
2004
1996
  let c_text = schema_to_c(treedb);
2005
1997
  let json_text = JSON.stringify(schema_to_json(treedb), null, 4);
2006
1998
 
@@ -2017,11 +2009,20 @@ function open_export(gobj, treedb)
2017
2009
  ["p", {class: "SCHEMA_EXPORT_HELP help mb-2",
2018
2010
  i18n: "an edit made here works and lives in no source: this is what to paste back"},
2019
2011
  t("an edit made here works and lives in no source: this is what to paste back")],
2012
+ drafts
2013
+ ? ["p", {class: "SCHEMA_EXPORT_DRAFTS notification is-warning is-light p-2 mb-2 is-size-7",
2014
+ i18n: "unsaved changes: save first, so the literal carries the versions that publish it"},
2015
+ t("unsaved changes: save first, so the literal carries the versions that publish it")]
2016
+ : ["span", {}, ""],
2020
2017
  ["textarea", {class: "SCHEMA_EXPORT_TEXT textarea is-small is-family-monospace",
2021
- rows: "18", readonly: "readonly", spellcheck: "false"}, c_text],
2018
+ rows: "18", readonly: "readonly", spellcheck: "false",
2019
+ title: t("schema"), "data-i18n-title": "schema",
2020
+ "aria-label": t("schema"), "data-i18n-aria-label": "schema"}, c_text],
2022
2021
  ["div", {class: "SCHEMA_EXPORT_ACTIONS is-flex mt-3",
2023
2022
  style: "gap:.5rem; justify-content:flex-end;"}, [
2024
- ["button", {class: "SCHEMA_EXPORT_COPY button", type: "button"}, [
2023
+ ["button", {class: "SCHEMA_EXPORT_COPY button", type: "button",
2024
+ title: t("copy"), "data-i18n-title": "copy",
2025
+ "aria-label": t("copy"), "data-i18n-aria-label": "copy"}, [
2025
2026
  ["span", {class: "icon"}, [["i", {class: "yi-copy"}]]],
2026
2027
  ["span", {i18n: "copy"}, t("copy")]
2027
2028
  ]]
@@ -2240,51 +2241,6 @@ function open_orphans(gobj)
2240
2241
 
2241
2242
 
2242
2243
 
2243
- /***************************************************************
2244
- * THE TWO VERSIONS A SCHEMA EDIT HAS TO RAISE, and why both.
2245
- *
2246
- * `topic_version` is what publishes a change of a topic's
2247
- * columns: leave it and the persisted `topic_cols.json` masks
2248
- * the edit, the restart succeeds and nothing moved.
2249
- *
2250
- * `schema_version` is what publishes the schema as a whole —
2251
- * "the stored one wins on ties, and the incoming one has to be
2252
- * strictly newer to take over" (c_treedb.c). It is safe to
2253
- * raise: re-projection from C compares `c_schema_version`, the
2254
- * version of the LITERAL, precisely so that an edit made here
2255
- * survives every start until a newer literal arrives.
2256
- *
2257
- * So every write that changes a schema carries both, and the
2258
- * operator is never asked to remember either.
2259
- ***************************************************************/
2260
- function version_writes(gobj, topic)
2261
- {
2262
- let treedb = current_treedb(gobj);
2263
- let writes = [];
2264
-
2265
- if(topic) {
2266
- let version = Number(topic.topic_version);
2267
- if(!isFinite(version)) {
2268
- version = 0;
2269
- }
2270
- writes.push({
2271
- op: "update", topic_name: T_TOPICS,
2272
- record: {id: topic.id, topic_version: version + 1}
2273
- });
2274
- }
2275
- if(treedb) {
2276
- let version = Number(treedb.schema_version);
2277
- if(!isFinite(version)) {
2278
- version = 0;
2279
- }
2280
- writes.push({
2281
- op: "update", topic_name: T_TREEDBS,
2282
- record: {id: treedb.id, schema_version: version + 1}
2283
- });
2284
- }
2285
- return writes;
2286
- }
2287
-
2288
2244
  /***************************************************************
2289
2245
  * WHERE THE COLUMNS WERE before this session started moving them.
2290
2246
  *
@@ -2968,7 +2924,7 @@ function ac_save_column(gobj, event, kw, src)
2968
2924
  op: kw.creating ? "create" : "update",
2969
2925
  topic_name: T_COLS,
2970
2926
  record: column_record(gobj, topic, col, values)
2971
- }].concat(version_writes(gobj, topic)));
2927
+ }]);
2972
2928
  }
2973
2929
 
2974
2930
  function ac_delete_column(gobj, event, kw, src)
@@ -3026,7 +2982,7 @@ function ac_move_column(gobj, event, kw, src)
3026
2982
  if(writes.length === 0) {
3027
2983
  return 0; /* dropped where it already was */
3028
2984
  }
3029
- return queue_writes(gobj, writes.concat(version_writes(gobj, topic)));
2985
+ return queue_writes(gobj, writes);
3030
2986
  }
3031
2987
 
3032
2988
  /***************************************************************
@@ -3055,7 +3011,7 @@ function ac_undo_order(gobj, event, kw, src)
3055
3011
  }
3056
3012
  /* Spent: what it named is where the columns are about to be. */
3057
3013
  delete priv.order_undo[topic.id];
3058
- return queue_writes(gobj, writes.concat(version_writes(gobj, topic)));
3014
+ return queue_writes(gobj, writes);
3059
3015
  }
3060
3016
 
3061
3017
  /***************************************************************
@@ -3127,7 +3083,7 @@ function ac_save_topic(gobj, event, kw, src)
3127
3083
  op: kw.creating ? "create" : "update",
3128
3084
  topic_name: T_TOPICS,
3129
3085
  record: topic_record(gobj, treedb, topic, values)
3130
- }].concat(version_writes(gobj, null)));
3086
+ }]);
3131
3087
  }
3132
3088
 
3133
3089
  function ac_delete_topic(gobj, event, kw, src)
@@ -3148,25 +3104,6 @@ function ac_delete_topic(gobj, event, kw, src)
3148
3104
  return 0;
3149
3105
  }
3150
3106
 
3151
- /***************************************************************
3152
- * Raising a version by hand: what the banner offers when
3153
- * something else wrote the topic and left its version alone.
3154
- ***************************************************************/
3155
- function ac_bump_version(gobj, event, kw, src)
3156
- {
3157
- let priv = gobj.priv;
3158
- let topic = find_topic(priv.model, priv.treedb_id, kw.topic);
3159
-
3160
- if(refuse_if_readonly(gobj, event)) {
3161
- return -1;
3162
- }
3163
- if(!topic) {
3164
- log_error(`${gobj_short_name(gobj)}: ${event} names no topic: ${kw.topic}`);
3165
- return -1;
3166
- }
3167
- return queue_writes(gobj, version_writes(gobj, topic));
3168
- }
3169
-
3170
3107
  /***************************************************************
3171
3108
  * Deleting a leftover.
3172
3109
  ***************************************************************/
@@ -3339,7 +3276,7 @@ function ac_confirmed(gobj, event, kw, src)
3339
3276
  return {op: "delete", topic_name: T_COLS, record: {id: col.id}};
3340
3277
  });
3341
3278
  writes.push({op: "delete", topic_name: T_TOPICS, record: {id: topic.id}});
3342
- return queue_writes(gobj, writes.concat(version_writes(gobj, null)));
3279
+ return queue_writes(gobj, writes);
3343
3280
  }
3344
3281
  if(kw.what === "column") {
3345
3282
  let topic = find_topic(priv.model, priv.treedb_id, kw.topic);
@@ -3350,7 +3287,7 @@ function ac_confirmed(gobj, event, kw, src)
3350
3287
  }
3351
3288
  return queue_writes(gobj, [{
3352
3289
  op: "delete", topic_name: T_COLS, record: {id: col.id}
3353
- }].concat(version_writes(gobj, topic)));
3290
+ }]);
3354
3291
  }
3355
3292
 
3356
3293
  log_error(`${gobj_short_name(gobj)}: ${event} of nothing this view asked about: ${kw.what}`);
@@ -3450,8 +3387,7 @@ function create_gclass(gclass_name)
3450
3387
  ["EV_DELETE_COLUMN", ac_delete_column, null],
3451
3388
  ["EV_SAVE_COLUMN", ac_save_column, null],
3452
3389
  ["EV_MOVE_COLUMN", ac_move_column, null],
3453
- ["EV_UNDO_ORDER", ac_undo_order, null],
3454
- ["EV_BUMP_VERSION", ac_bump_version, null]
3390
+ ["EV_UNDO_ORDER", ac_undo_order, null]
3455
3391
  ])],
3456
3392
 
3457
3393
  /* A write, or a plan of them, in flight. Only the answers and
@@ -3494,7 +3430,6 @@ function create_gclass(gclass_name)
3494
3430
  ["EV_SAVE_COLUMN", 0],
3495
3431
  ["EV_MOVE_COLUMN", 0],
3496
3432
  ["EV_UNDO_ORDER", 0],
3497
- ["EV_BUMP_VERSION", 0],
3498
3433
  ["EV_VALIDATE", 0],
3499
3434
  ["EV_EXPORT", 0],
3500
3435
  ["EV_IMPORT", 0],
@@ -3549,5 +3484,5 @@ export {
3549
3484
  register_c_yui_schema_editor,
3550
3485
  /* Exported for its test: the rule that decides whether a topic still
3551
3486
  * owes a version bump, which is asked in two places. */
3552
- needs_version_bump
3487
+ topic_is_draft
3553
3488
  };
@@ -111,6 +111,7 @@ import {delete_impact} from "./delete_impact.js";
111
111
 
112
112
  import "./c_yui_treedb_topic_with_form.css";
113
113
  import "./tabulator.css";
114
+ import {cell_text, hook_cell_spec} from "./table_cell_text.js";
114
115
 
115
116
  import { TabulatorFull as Tabulator } from "tabulator-tables";
116
117
 
@@ -2147,20 +2148,12 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field)
2147
2148
  let items = treedb_hook_data_size(value);
2148
2149
 
2149
2150
  if(items > 0) {
2150
- /* An HTML STRING, so the name is escaped by hand; a language
2151
- * change re-runs setColumns(), which re-runs this. */
2152
- let hook_title = String(t("show linked records"))
2153
- .replace(/&/g, "&").replace(/"/g, """)
2154
- .replace(/</g, "&lt;").replace(/>/g, "&gt;");
2155
- value = [
2156
- '<a class="hook_cell" ',
2157
- `title="${hook_title}" aria-label="${hook_title}" `,
2158
- `data-row_id="${row.id}" `,
2159
- `data-col_id="${col.id}" > `,
2160
- '<span style="" class="icon yi-eye"></span>',
2161
- `<span>[&nbsp;<u>${items}</u>&nbsp;]</span>`,
2162
- '</a>'
2163
- ].join('');
2151
+ /* DOM, not an HTML string: the row id is record data (M31
2152
+ * of the 2026-09-21 review). A language change re-runs
2153
+ * setColumns(), which re-runs this. */
2154
+ value = createElement2(hook_cell_spec(
2155
+ row.id, col.id, items, t("show linked records"), "show linked records"
2156
+ ));
2164
2157
  } else {
2165
2158
  value = "";
2166
2159
  }
@@ -2217,7 +2210,7 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field)
2217
2210
  };
2218
2211
 
2219
2212
  const formatter = new Intl.DateTimeFormat(userLocale, opts);
2220
- value = '<span class="is-size-7">' + formatter.format(value) + '</span>';
2213
+ value = createElement2(['span', {class: 'is-size-7'}, formatter.format(value)]);
2221
2214
  break;
2222
2215
  }
2223
2216
  break;
@@ -2249,6 +2242,15 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field)
2249
2242
  value = JSON.stringify(value);
2250
2243
  }
2251
2244
 
2245
+ /*
2246
+ * A string is record data, and Tabulator would parse it as markup
2247
+ * (innerHTML): `a<b` lost its text, a field holding an `<img onerror>`
2248
+ * ran it (M31 of the 2026-09-21 review). Shown as a text node.
2249
+ */
2250
+ if(typeof value === "string" && value !== "") {
2251
+ value = cell_text(value);
2252
+ }
2253
+
2252
2254
  return value;
2253
2255
  }
2254
2256
 
@@ -0,0 +1,46 @@
1
+ /****************************************************************************
2
+ * form_time_value.js
3
+ *
4
+ * A treedb `time` / `now` column is an epoch in SECONDS, and a form
5
+ * shows it in a `datetime-local` input. These two convert between
6
+ * them, and they must be each other's inverse: a writable time
7
+ * column travels back on EVERY save of the record.
8
+ *
9
+ * Copyright (c) 2026, ArtGins.
10
+ * All Rights Reserved.
11
+ ****************************************************************************/
12
+
13
+ /************************************************************
14
+ * A Date -> the value of a `datetime-local` input, in local time,
15
+ * WITH seconds: the input is built with `step: 1`, which is what
16
+ * lets it hold them. Written as "YYYY-MM-DDTHH:mm" it lost them on
17
+ * every save of any field (M26 of the 2026-09-21 review).
18
+ ************************************************************/
19
+ function date_to_datetime_local(value)
20
+ {
21
+ if(!(value instanceof Date) || isNaN(value.getTime())) {
22
+ return "";
23
+ }
24
+ let pad = (n) => String(n).padStart(2, "0");
25
+ return `${value.getFullYear()}-${pad(value.getMonth() + 1)}` +
26
+ `-${pad(value.getDate())}T` +
27
+ `${pad(value.getHours())}:${pad(value.getMinutes())}` +
28
+ `:${pad(value.getSeconds())}`;
29
+ }
30
+
31
+ /************************************************************
32
+ * The value of a `datetime-local` input -> epoch in seconds
33
+ ************************************************************/
34
+ function datetime_local_to_epoch(value)
35
+ {
36
+ if(!value) {
37
+ return null;
38
+ }
39
+ const date = new Date(value);
40
+ if(isNaN(date.getTime())) {
41
+ return null;
42
+ }
43
+ return Math.floor(date.getTime() / 1000);
44
+ }
45
+
46
+ export {date_to_datetime_local, datetime_local_to_epoch};
@@ -0,0 +1,23 @@
1
+ import {describe, it, expect} from "vitest";
2
+ import {date_to_datetime_local, datetime_local_to_epoch} from "./form_time_value.js";
3
+
4
+ describe("form_time_value", () => {
5
+
6
+ it("an epoch with seconds survives a round trip through the input", () => {
7
+ /* M26 of the 2026-09-21 review: the input was written without
8
+ * seconds, so every save of ANY field moved a writable time
9
+ * column back up to 59 s. */
10
+ const epoch = 1790000037; // :37 seconds
11
+ const shown = date_to_datetime_local(new Date(epoch * 1000));
12
+ expect(datetime_local_to_epoch(shown)).toBe(epoch);
13
+ });
14
+
15
+ it("an empty input is null, not NaN", () => {
16
+ expect(datetime_local_to_epoch("")).toBe(null);
17
+ });
18
+
19
+ it("no date shows as an empty input", () => {
20
+ expect(date_to_datetime_local(null)).toBe("");
21
+ expect(date_to_datetime_local(new Date(NaN))).toBe("");
22
+ });
23
+ });
@@ -16,10 +16,11 @@
16
16
  * operator drew does nothing at all. Restarting on
17
17
  * this is an outage with no gain.
18
18
  * `warning` the schema opens and something is not what it looks
19
- * like — an unbumped `topic_version` masking the whole
20
- * edit is the one that costs the most time, because
21
- * the restart SUCCEEDS and the change is simply not
22
- * there.
19
+ * like.
20
+ *
21
+ * A version is not checked here: an edit is a DRAFT and moves no
22
+ * version, and C_TREEDB's save-schema raises the versions of what
23
+ * changed when it publishes it.
23
24
  *
24
25
  * Every `code` is an i18n key: the caller renders, this decides.
25
26
  *
@@ -69,8 +70,6 @@ function validate_schema(treedb, options)
69
70
  {
70
71
  let findings = [];
71
72
  let opts = options || {};
72
- let written = Array.isArray(opts.written_topics) ? opts.written_topics : [];
73
- let baseline = opts.baseline || {};
74
73
 
75
74
  if(!treedb || !Array.isArray(treedb.topics)) {
76
75
  return findings;
@@ -117,17 +116,6 @@ function validate_schema(treedb, options)
117
116
  }
118
117
  }
119
118
 
120
- /* A version that did not move republishes nothing: the stored
121
- * topic_cols.json masks the whole edit and the restart looks
122
- * like it worked. */
123
- if(written.indexOf(topic.id) >= 0) {
124
- let before = baseline[topic.id];
125
- if(before !== undefined && String(before) === String(topic.topic_version)) {
126
- add("warning", "topic version not bumped", topic.name, "",
127
- String(topic.topic_version));
128
- }
129
- }
130
-
131
119
  for(let col of topic.cols) {
132
120
  let flags = col_flags(col.record);
133
121
  let type = col.record ? col.record.type : "";
@@ -221,34 +221,14 @@ describe("the column itself", () => {
221
221
  });
222
222
  });
223
223
 
224
- describe("the version that did not move", () => {
225
- test("a written topic whose topic_version is unchanged is reported", () => {
224
+ describe("a version is not the validator's", () => {
225
+ test("a written topic whose topic_version did not move is NOT reported", () => {
226
+ /* An edit is a draft; save-schema raises the version. */
226
227
  const f = validate_schema(schema(), {
227
228
  written_topics: ["db.departments"],
228
229
  baseline: {"db.departments": 1}
229
230
  });
230
- const v = f.filter(x => x.code === "topic version not bumped");
231
- expect(v.length).toBe(1);
232
- expect(v[0].topic).toBe("departments");
233
- expect(v[0].severity).toBe("warning");
234
- });
235
-
236
- test("a bumped version says nothing", () => {
237
- expect(validate_schema(schema({dep_version: 2}), {
238
- written_topics: ["db.departments"],
239
- baseline: {"db.departments": 1}
240
- })).toEqual([]);
241
- });
242
-
243
- test("a topic nobody wrote is not asked about its version", () => {
244
- expect(validate_schema(schema(), {baseline: {"db.departments": 1}})).toEqual([]);
245
- });
246
-
247
- test("a version compared across types still matches (2 vs '2')", () => {
248
- expect(validate_schema(schema({dep_version: "1"}), {
249
- written_topics: ["db.departments"],
250
- baseline: {"db.departments": 1}
251
- }).length).toBe(1);
231
+ expect(f.filter(x => x.code === "topic version not bumped")).toEqual([]);
252
232
  });
253
233
  });
254
234
 
@@ -1,92 +1,35 @@
1
1
  /***********************************************************************
2
2
  * schema_version_bump.test.js
3
3
  *
4
- * "This topic was written and its version did not move."
4
+ * "This topic was written in this session and is not saved."
5
5
  *
6
- * THE RULE MEASURES AGAINST A BASELINE, AND THE BASELINE MUST NOT
7
- * MOVE WITH IT. It used to be re-taken inside build_model(), which
8
- * runs again on the patch after EVERY write -- so it came back as
9
- * the version the write had just raised, the rule read TRUE for a
10
- * topic whose version HAD moved, and the banner that says so
11
- * offered a Raise button that wrote another version and was
12
- * measured against itself again.
13
- *
14
- * The banner could not be dismissed. One operator pressed it
15
- * thirty-three times on a single column delete, and that is why
16
- * schema versions climbed by tens with the schema unchanged.
6
+ * An edit is a DRAFT (M36 of yunetas' 2026-09-21 review): it moves
7
+ * no version, and the host's save-schema raises the versions of what
8
+ * changed. So "written" alone is the whole question -- the version is
9
+ * no longer measured against a baseline, which is also why the old
10
+ * "Raise" button and the loop it fed are gone.
17
11
  *
18
12
  * Copyright (c) 2026, ArtGins.
19
13
  * All Rights Reserved.
20
14
  ***********************************************************************/
21
15
  import {describe, it, expect} from "vitest";
22
- import {needs_version_bump} from "./c_yui_schema_editor.js";
16
+ import {topic_is_draft} from "./c_yui_schema_editor.js";
23
17
 
24
18
  const TOPIC = {id: "device_groups", topic_version: 5};
25
19
 
26
- describe("needs_version_bump", () => {
20
+ describe("topic_is_draft", () => {
27
21
  it("says nothing about a topic this session never wrote", () => {
28
- expect(needs_version_bump({}, {device_groups: 5}, TOPIC)).toBe(false);
22
+ expect(topic_is_draft({}, TOPIC)).toBe(false);
29
23
  });
30
24
 
31
- it("asks for the bump when the version sits where it was found", () => {
32
- expect(needs_version_bump({device_groups: true}, {device_groups: 5}, TOPIC))
25
+ it("a written topic is a draft, whatever its version says", () => {
26
+ expect(topic_is_draft({device_groups: true}, TOPIC)).toBe(true);
27
+ expect(topic_is_draft({device_groups: true}, {id: "device_groups", topic_version: 6}))
33
28
  .toBe(true);
34
29
  });
35
30
 
36
- it("STOPS asking once the version has moved past the baseline", () => {
37
- /* The case the loop got wrong: the write raised it to 6 and the
38
- * baseline still says 5, so there is nothing left to raise. */
39
- expect(needs_version_bump(
40
- {device_groups: true}, {device_groups: 5}, {id: "device_groups", topic_version: 6}
41
- )).toBe(false);
42
- });
43
-
44
- it("compares as text, so 5 and '5' are the same version", () => {
45
- expect(needs_version_bump(
46
- {device_groups: true}, {device_groups: "5"}, TOPIC
47
- )).toBe(true);
48
- expect(needs_version_bump(
49
- {device_groups: true}, {device_groups: 5}, {id: "device_groups", topic_version: "5"}
50
- )).toBe(true);
51
- });
52
-
53
31
  it("answers false rather than throwing on a missing half", () => {
54
- expect(needs_version_bump(null, {device_groups: 5}, TOPIC)).toBe(false);
55
- expect(needs_version_bump({device_groups: true}, null, TOPIC)).toBe(false);
56
- expect(needs_version_bump({device_groups: true}, {device_groups: 5}, null)).toBe(false);
57
- });
58
-
59
- it("a topic with no baseline entry is not owed a bump", () => {
60
- expect(needs_version_bump({device_groups: true}, {}, TOPIC)).toBe(false);
61
- });
62
- });
63
-
64
- describe("the loop it came from", () => {
65
- /*
66
- * What the editor does per bump: raise the version by one. With the
67
- * baseline left alone, ONE press settles it; with the baseline
68
- * re-taken each time -- the old bug -- it never settles.
69
- */
70
- function press_raise(state) {
71
- return {written: state.written, baseline: state.baseline,
72
- topic: {id: "t", topic_version: Number(state.topic.topic_version) + 1}};
73
- }
74
-
75
- it("settles after a single press", () => {
76
- let st = {written: {t: true}, baseline: {t: 5}, topic: {id: "t", topic_version: 5}};
77
- expect(needs_version_bump(st.written, st.baseline, st.topic)).toBe(true);
78
- st = press_raise(st);
79
- expect(needs_version_bump(st.written, st.baseline, st.topic)).toBe(false);
80
- });
81
-
82
- it("never settles if the baseline is re-taken with the version", () => {
83
- let st = {written: {t: true}, baseline: {t: 5}, topic: {id: "t", topic_version: 5}};
84
- let presses = 0;
85
- while(needs_version_bump(st.written, st.baseline, st.topic) && presses < 40) {
86
- st = press_raise(st);
87
- st.baseline = {t: st.topic.topic_version}; /* the bug */
88
- presses++;
89
- }
90
- expect(presses).toBe(40); /* it ran to the guard: no way out */
32
+ expect(topic_is_draft(null, TOPIC)).toBe(false);
33
+ expect(topic_is_draft({device_groups: true}, null)).toBe(false);
91
34
  });
92
35
  });