tina4ruby 3.13.7 → 3.13.11

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6526c615d8d8dd6c8bdba089bceb5969917b2c4400d44886f3c3d757ad4ef14f
4
- data.tar.gz: ddebcbd9f847f2660b6ed32e6f7ef86b7e3bd5aa1df3f37f2827d4a2549d9efb
3
+ metadata.gz: a23c9445f57fd1c84a7bed4e5325a26f94f2234d703f889ffea769faefa5622e
4
+ data.tar.gz: 7082c808b1d72968997c69167b734cea292e2616c086b46b05831254f30f0353
5
5
  SHA512:
6
- metadata.gz: 066d4124d585f5e79c3b41e3a763dc655c68d45e5c4afb19c01e0c5dc50fec29f50e21c0fe2eb3b6c8a9941664e45134eb46ef21c42827298c729a77801e766e
7
- data.tar.gz: '0148be259f7b00d815abfc04b35f7bbca09e0e5561ff24e72e4a94a4e9e2c7d94236f67949e31d48daba76d383550a12f0c67d690a03a107db7642ef2a9e5570'
6
+ metadata.gz: f9896ecf33201656f977ceb7bdf4739f2585b14ec32e2c969f61b5ae3c24d69b007ce02d79b053dd96be680af7a5eed8bac8f262fab43e0fc1c51720ff3b2cae
7
+ data.tar.gz: 9722548ba9dcc819407c20d426154eab388f092424e2d1a9bcd253a877bf79579e5937ee98d319adf2f736aceee0ce57e24c939755a38db070109df9bca76819
data/lib/tina4/ai.rb CHANGED
@@ -135,6 +135,118 @@ module Tina4
135
135
  # @param tool [Hash] tool entry from AI_TOOLS
136
136
  # @param context [String] generated context content
137
137
  # @return [Array<String>] list of created/updated relative file paths
138
+ # ── v3.13.9: non-destructive context-file writer ─────────────────
139
+ #
140
+ # Pre-v3.13.9 the installer wrote a full developer guide to
141
+ # CLAUDE.md (and the other context files) on every run, clobbering
142
+ # whatever the user had put there. Now it writes only a marker-
143
+ # bracketed Tina4 skill block -- pointing the assistant at
144
+ # .claude/skills/tina4-*/SKILL.md -- and leaves the rest alone.
145
+
146
+ # Return [start, end] markers for the given context file.
147
+ def markers_for(context_file)
148
+ if context_file.downcase.end_with?(".md")
149
+ ["<!-- tina4-skills:start -->", "<!-- tina4-skills:end -->"]
150
+ else
151
+ ["# tina4-skills:start", "# tina4-skills:end"]
152
+ end
153
+ end
154
+
155
+ # Return the marker-bracketed Tina4 skill registration block.
156
+ def skill_block(context_file)
157
+ start, finish = markers_for(context_file)
158
+ body = if context_file.downcase.end_with?(".md")
159
+ "## Tina4 Skills\n\n" \
160
+ "When working on this Tina4 project, these skills give the assistant project-aware behaviour:\n\n" \
161
+ "- **tina4-developer** -- Read `.claude/skills/tina4-developer/SKILL.md` before building features.\n" \
162
+ "- **tina4-js** -- Read `.claude/skills/tina4-js/SKILL.md` for frontend work.\n" \
163
+ "- **tina4-maintainer** -- Read `.claude/skills/tina4-maintainer/SKILL.md` for framework-level changes.\n\n" \
164
+ "See https://tina4.com for full docs."
165
+ else
166
+ "Tina4 Skills -- read these files before working on this project:\n" \
167
+ " .claude/skills/tina4-developer/SKILL.md (feature development)\n" \
168
+ " .claude/skills/tina4-js/SKILL.md (frontend / tina4-js)\n" \
169
+ " .claude/skills/tina4-maintainer/SKILL.md (framework-level changes)\n" \
170
+ "Docs: https://tina4.com"
171
+ end
172
+ "#{start}\n#{body}\n#{finish}"
173
+ end
174
+
175
+ # True iff both start and end markers appear in order.
176
+ def has_markers?(existing, start, finish)
177
+ s_idx = existing.index(start)
178
+ return false unless s_idx
179
+ !existing.index(finish, s_idx + start.length).nil?
180
+ end
181
+
182
+ # Replace the bracketed block in `existing` with `block`.
183
+ def replace_marker_block(existing, block, start, finish)
184
+ s_idx = existing.index(start)
185
+ return existing.rstrip + "\n\n" + block + "\n" unless s_idx
186
+ e_idx = existing.index(finish, s_idx + start.length)
187
+ return existing.rstrip + "\n\n" + block + "\n" unless e_idx
188
+ before = existing[0...s_idx].rstrip
189
+ after = existing[(e_idx + finish.length)..].sub(/\A\n+/, "")
190
+ glue_before = before.empty? ? "" : "\n\n"
191
+ glue_after = after.empty? ? "\n" : "\n" + after
192
+ "#{before}#{glue_before}#{block}#{glue_after}"
193
+ end
194
+
195
+ # Headers the pre-v3.13.9 installer wrote at the top of CLAUDE.md.
196
+ OLD_FRAMEWORK_HEADERS = [
197
+ "# Tina4 Python",
198
+ "# Tina4 PHP",
199
+ "# Tina4 Ruby",
200
+ "# CLAUDE.md -- AI Developer Guide for tina4-nodejs",
201
+ "# CLAUDE.md — AI Developer Guide for tina4-nodejs",
202
+ ].freeze
203
+
204
+ def looks_like_old_framework_install?(existing)
205
+ head = existing.lstrip[0, 400] || ""
206
+ OLD_FRAMEWORK_HEADERS.any? { |h| head.start_with?(h) }
207
+ end
208
+
209
+ # Write the context file non-destructively. Returns a human-readable
210
+ # action verb for the caller's log line.
211
+ #
212
+ # Four branches:
213
+ # 1. Doesn't exist -> write framework guide + skill block
214
+ # 2. Has markers -> refresh just the skill block (idempotent)
215
+ # 3. Old header -> migrate: replace old dump with new guide + block
216
+ # 4. User content -> append the skill block, preserve everything else
217
+ def write_or_merge(context_path, context_file, framework_guide)
218
+ # Force UTF-8 — CLAUDE.md and the framework guide both contain
219
+ # non-ASCII (em-dashes, ✓, etc.). Without this, File.read may
220
+ # return ASCII-8BIT and string concat raises CompatibilityError.
221
+ block = skill_block(context_file).dup.force_encoding("UTF-8")
222
+ guide = framework_guide.dup.force_encoding("UTF-8")
223
+ start, finish = markers_for(context_file)
224
+
225
+ unless File.exist?(context_path)
226
+ File.write(context_path, guide.rstrip + "\n\n" + block + "\n", encoding: "UTF-8")
227
+ return "Installed"
228
+ end
229
+
230
+ existing = File.read(context_path, encoding: "UTF-8")
231
+
232
+ if has_markers?(existing, start, finish)
233
+ File.write(context_path, replace_marker_block(existing, block, start, finish), encoding: "UTF-8")
234
+ return "Refreshed skill block in"
235
+ end
236
+
237
+ if looks_like_old_framework_install?(existing)
238
+ head = existing.lstrip
239
+ preamble = existing[0, existing.length - head.length] || ""
240
+ new_content = (preamble.strip.empty? ? "" : preamble.rstrip + "\n\n") +
241
+ guide.rstrip + "\n\n" + block + "\n"
242
+ File.write(context_path, new_content, encoding: "UTF-8")
243
+ return "Migrated (replaced old framework dump in)"
244
+ end
245
+
246
+ File.write(context_path, existing.rstrip + "\n\n" + block + "\n", encoding: "UTF-8")
247
+ "Appended skill block to"
248
+ end
249
+
138
250
  def install_for_tool(root, tool, context)
139
251
  created = []
140
252
  context_path = File.join(root, tool[:context_file])
@@ -145,9 +257,8 @@ module Tina4
145
257
  end
146
258
  FileUtils.mkdir_p(File.dirname(context_path))
147
259
 
148
- # Always overwrite -- user chose to install
149
- action = File.exist?(context_path) ? "Updated" : "Installed"
150
- File.write(context_path, context)
260
+ # v3.13.9: non-destructive write -- see write_or_merge below.
261
+ action = write_or_merge(context_path, tool[:context_file], context)
151
262
  rel = context_path.sub("#{root}/", "")
152
263
  created << rel
153
264
  puts " \e[32m✓\e[0m #{action} #{rel}"
data/lib/tina4/orm.rb CHANGED
@@ -302,13 +302,27 @@ module Tina4
302
302
  def create_table
303
303
  return true if db.table_exists?(table_name)
304
304
 
305
+ # v3.13.11 (BooleanField parity): pick each engine's native
306
+ # bool type where it's reliable. SQLite has no native bool;
307
+ # Firebird's driver round-trip for native BOOLEAN is uneven —
308
+ # both stay on INTEGER. PG/MySQL/MSSQL use their native types
309
+ # so Python ``True``/Ruby ``true`` bind cleanly without
310
+ # ``operator does not exist: boolean = integer`` errors.
311
+ engine = (db.respond_to?(:get_database_type) ? db.get_database_type : "").to_s.downcase
312
+ bool_sql = case engine
313
+ when "postgres", "postgresql" then "BOOLEAN"
314
+ when "mysql" then "BOOLEAN" # alias for TINYINT(1)
315
+ when "mssql", "sqlserver" then "BIT"
316
+ else "INTEGER" # sqlite, firebird, odbc, anything else
317
+ end
318
+
305
319
  type_map = {
306
320
  integer: "INTEGER",
307
321
  string: "VARCHAR(255)",
308
322
  text: "TEXT",
309
323
  float: "REAL",
310
324
  decimal: "REAL",
311
- boolean: "INTEGER",
325
+ boolean: bool_sql,
312
326
  date: "DATE",
313
327
  datetime: "DATETIME",
314
328
  timestamp: "TIMESTAMP",
@@ -419,10 +433,18 @@ module Tina4
419
433
  setter = "#{key}="
420
434
  __send__(setter, value) if respond_to?(setter)
421
435
  end
422
- # Set defaults
436
+ # Set defaults.
437
+ # v3.13.11 (issue #50.1): when the default is a Proc/lambda
438
+ # (``default: -> { Time.now }``), call it per-instance so
439
+ # per-row timestamps actually differ. Class objects are
440
+ # excluded — ``default: Integer`` is almost never intended
441
+ # to mean ``Integer.new`` (and Integer has no zero-arg
442
+ # constructor anyway).
423
443
  self.class.field_definitions.each do |name, opts|
424
444
  if __send__(name).nil? && opts[:default]
425
- __send__("#{name}=", opts[:default])
445
+ d = opts[:default]
446
+ d = d.call if d.respond_to?(:call) && !d.is_a?(Class)
447
+ __send__("#{name}=", d)
426
448
  end
427
449
  end
428
450
  end
data/lib/tina4/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "3.13.7"
4
+ VERSION = "3.13.11"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tina4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.13.7
4
+ version: 3.13.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-10 00:00:00.000000000 Z
11
+ date: 2026-06-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack