tina4ruby 3.13.118 → 3.13.120

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: 8e78af521f04b7255908e1ac3420b7f15a675b9d8f7b02850d307c3d2ab5cb24
4
- data.tar.gz: 428249645823e6047d1bc7168304fb40afcc0621303d6b22fc6c35b893080759
3
+ metadata.gz: 95ef84b463d6357070f1f1261556d5d6dc44e7355d279ee987919789b2b11ca2
4
+ data.tar.gz: dd408c271fd30de5ccb6f17c16ff116f0882e39f7f60775d298570e48c201294
5
5
  SHA512:
6
- metadata.gz: 87d861452214e641ae813a13a9d17c6c00e8dc833dc679e5c0c02a356f4e7038e63f0d6bffde6c402d1bac1d83fbf65243266db94843b93949d4adbf117bc1db
7
- data.tar.gz: f3f235a7574d6ce013346ca26d5cc2e04c7b3c9c2920bf5fc9635ae8eefe5665b30c95d6eeb802f74b1b3de3279321e531f3a3c20baced7cac1d2df1e24e4c33
6
+ metadata.gz: e7a9dd4f5ecf64590747b052f67588194e4464d81dfe5f9455bb968132267951ed451915b6605d232688dd803c8deda7eb32f631fee4adb6672ca373acc8146e
7
+ data.tar.gz: 4431a908d43403601b76d82fb5bac6ad36948d532fce131847f14d0cbbc1c45f46a4b9785613cb5f3588553301f257eb42e6d5f68bd4b4473f7c24665d52dbde
data/CHANGELOG.md CHANGED
@@ -6,6 +6,34 @@ number means the same thing everywhere.
6
6
  **The authoritative release notes for every shipped version live in the documentation:**
7
7
  https://tina4.com/ruby/36-releases
8
8
 
9
+ ## 3.13.119
10
+
11
+ Skill repair by @MichaelC8E (#44, merged as c1b8c43d5) plus a
12
+ small CLAUDE.md footer bump that the 3.13.118 release worker missed.
13
+
14
+ ### Skill file repairs (#44)
15
+
16
+ Three defects fixed across the three skill trees this repo carries
17
+ (`.claude/skills/`, `.agents/skills/`, `.cursor/skills/`):
18
+
19
+ - The Codex and Cursor copies of `tina4-maintainer` were UTF-8-with-BOM
20
+ with both em dashes replaced by the cp1252 round-trip
21
+ `c3 a2 e2 82 ac e2 80 9d`. Corruption sat inside the `description`
22
+ frontmatter (the text that decides when a skill triggers), and the
23
+ BOM sat in front of the opening `---` (which some frontmatter
24
+ parsers reject outright). All eight tracked copies across the
25
+ language ports were byte-identical, so every diff-based check
26
+ reported them clean.
27
+ - Two shared files had gone stale against canonical in tina4-python.
28
+ - The Codex and Cursor copies of `tina4-developer-ruby` were around
29
+ 60 lines behind `.claude`, and shipped none of the seven
30
+ `references/` files their own SKILL.md cites.
31
+
32
+ Parity: tina4-python 3.13.119 and tina4-nodejs 3.13.119 are
33
+ version-parity bumps. tina4-php 3.13.119 ships @MichaelC8E's
34
+ skill-repair PR (tina4-php#205), @cwvermaak-codeinfinity's Messenger
35
+ fix (tina4-php#204), and my ImportHelper autoload non-throw fix.
36
+
9
37
  ## 3.13.118
10
38
 
11
39
  Version-parity bump. No framework code changes in Ruby for this
data/lib/tina4/cli.rb CHANGED
@@ -4,6 +4,8 @@ require "optparse"
4
4
  require "fileutils"
5
5
  require "set"
6
6
  require "json"
7
+ require "tmpdir" # Dir.mktmpdir — used by the ADR-0063 sandbox edit-hint scan
8
+ require "stringio" # StringIO — captures generator stdout during the sandbox run
7
9
  require_relative "port_takeover"
8
10
 
9
11
  module Tina4
@@ -289,6 +291,87 @@ module Tina4
289
291
  # created_at. The first write then failed with "no such column: name".
290
292
  DEFAULT_FIELDS = [["name", "string"]].freeze
291
293
 
294
+ # ADR-0063 (scaffolding envelope v1.1): `# tina4:edit <label>` in a generated
295
+ # Ruby file, or `-- tina4:edit <label>` in a generated SQL file, marks a
296
+ # first-edit spot for a developer / coding agent to fill in. The scanner
297
+ # surfaces every match as an `edit_hints` entry in the `generate_v1_1`
298
+ # envelope AND under "Edit these lines:" in the human stderr block. Any
299
+ # comment character (`#` or `--`) followed by `tina4:edit` and one or more
300
+ # spaces then the label matches — Twig `{# tina4:edit … #}` is Wave 2 and
301
+ # deliberately NOT matched here.
302
+ EDIT_MARKER_RE = /(?:#|--)\s*tina4:edit[ \t]+(.+?)\s*$/.freeze
303
+
304
+ # ADR-0063 next-step catalogue: 5 short actionable lines per resolution-aware
305
+ # verb. Rendered under "Next:" in the human block AND surfaced as
306
+ # `resolution.next[]` in the envelope. Each entry is a `Proc` that consumes
307
+ # the resolution so paths / class names substitute correctly. Curated (not
308
+ # generated) so a caller sees consistent, sensible next steps.
309
+ NEXT_STEPS = {
310
+ "model" => lambda do |res|
311
+ table = res["table_name"] || "resource"
312
+ class_name = res["class_name"] || "Model"
313
+ file_path = res["file_path"] || "src/orm/#{table}.rb"
314
+ route = (res["routes"] || []).first || "/#{table}"
315
+ [
316
+ "Edit #{file_path} to add fields beyond the default 'name'",
317
+ "Apply the migration: tina4ruby migrate",
318
+ "Try it: tina4ruby serve -> curl http://localhost:7147#{route}",
319
+ "Add CRUD scaffolding: tina4ruby generate crud #{class_name} --skip-model",
320
+ "Seed sample data: tina4ruby generate seeder #{class_name} && tina4ruby seed",
321
+ ]
322
+ end,
323
+ "route" => lambda do |res|
324
+ file_path = res["file_path"] || "src/routes/resource.rb"
325
+ route = (res["routes"] || []).first || "/api/resource"
326
+ test_path = (res["test_paths"] || []).first || "spec/routes/resource_spec.rb"
327
+ [
328
+ "Edit #{file_path} to customise the response",
329
+ "Try it: tina4ruby serve -> curl http://localhost:7147#{route}",
330
+ "Add real assertions: #{test_path}",
331
+ "Bearer-gate is on: writes require a Bearer token (rerun with --public to open)",
332
+ "Browse the API: tina4ruby serve -> http://localhost:7147/swagger",
333
+ ]
334
+ end,
335
+ "migration" => lambda do |res|
336
+ file_path = res["file_path"] || "migrations/new_migration.sql"
337
+ down_path = file_path.sub(/\.sql$/, ".down.sql")
338
+ [
339
+ "Edit #{file_path} to add columns beyond id + created_at",
340
+ "Mirror the change in: #{down_path}",
341
+ "Apply it: tina4ruby migrate",
342
+ "Check status: tina4ruby migrate status",
343
+ "Rollback if wrong: tina4ruby migrate rollback",
344
+ ]
345
+ end,
346
+ "middleware" => lambda do |res|
347
+ class_name = res["class_name"] || "Middleware"
348
+ file_path = res["file_path"] || "src/middleware/middleware.rb"
349
+ [
350
+ "Edit #{file_path} to guard the request or shape the response",
351
+ "Wire it into a route: middleware: [#{class_name}] on Tina4.get/post/...",
352
+ "Wire it globally: Tina4::Router.use(#{class_name})",
353
+ "Prove the gate: add a real spec covering allow + deny",
354
+ "Chain more: order in the middleware: [] list is registration order",
355
+ ]
356
+ end,
357
+ }.freeze
358
+
359
+ # ADR-0063: freeze `Time.now` for the duration of one `cmd_generate` call.
360
+ # Both the resolution builders (`build_migration_resolution`,
361
+ # `build_model_resolution`) and the actual generator (`generate_migration`)
362
+ # embed a timestamp in the migration filename. If they call `Time.now`
363
+ # independently, the sandbox pre-run (which drives edit_hints for dry-run
364
+ # mode) and the real run can pick different seconds, and the envelope's
365
+ # `file_path` would drift from the file that actually gets written. One
366
+ # memoised timestamp per command run fixes that.
367
+ def generate_timestamp
368
+ @generate_timestamp ||= Time.now
369
+ end
370
+
371
+ def reset_generate_timestamp!
372
+ @generate_timestamp = nil
373
+ end
374
+
292
375
  # Parsed --fields, or the default single `name` column when none given.
293
376
  def fields_or_default(fields_str)
294
377
  parsed = parse_fields(fields_str)
@@ -331,7 +414,11 @@ module Tina4
331
414
  bar = "─" * 60
332
415
  head = "#{indent}# ─── AI-FILL: #{fn} "
333
416
  head += "─" * [4, 66 - head.length].max
334
- lines = [head, "#{indent}# Intent: #{intent}"]
417
+ # ADR-0063: prepend a `# tina4:edit <label>` machine-readable pointer so a
418
+ # tool can surface WHERE this stub needs custom logic without parsing the
419
+ # AI-FILL banner. `intent` is the developer-facing TODO — reuse it as the
420
+ # label (short, actionable, no period).
421
+ lines = ["#{indent}# tina4:edit #{intent}", head, "#{indent}# Intent: #{intent}"]
335
422
  lines << "#{indent}# Given: #{given}" if given
336
423
  lines << "#{indent}# Use: #{use}"
337
424
  lines << "#{indent}# Return: #{ret}" if ret
@@ -348,7 +435,10 @@ module Tina4
348
435
  def extend_marker(note, hint = "", indent: " ")
349
436
  head = "#{indent}# ─── EXTEND: #{note} "
350
437
  head += "─" * [4, 66 - head.length].max
351
- out = head + "\n"
438
+ # ADR-0063: `# tina4:edit <label>` is the machine-readable partner of the
439
+ # ─── EXTEND banner. `note` is the human-shaped TODO — reuse it verbatim.
440
+ out = "#{indent}# tina4:edit #{note}\n"
441
+ out += head + "\n"
352
442
  out += "#{indent}# #{hint}\n" unless hint.to_s.empty?
353
443
  out
354
444
  end
@@ -1117,6 +1207,10 @@ module Tina4
1117
1207
  # will produce before it runs.
1118
1208
 
1119
1209
  def cmd_generate(argv)
1210
+ # ADR-0063: fresh timestamp per command run so the sandbox pre-run + the
1211
+ # real generator both agree on the migration filename.
1212
+ reset_generate_timestamp!
1213
+
1120
1214
  what = argv.shift
1121
1215
  all = GENERATORS.keys.join(", ") # single source: the GENERATORS registry
1122
1216
 
@@ -1126,7 +1220,7 @@ module Tina4
1126
1220
  puts ' Options: --fields "name:string,price:float" --model ModelName'
1127
1221
  puts ' --public open a route'"'"'s writes (default: secure)'
1128
1222
  puts ' --every 5m | --cron "..." service schedule'
1129
- puts ' --json emit the generate_v1 resolution envelope'
1223
+ puts ' --json emit the generate_v1_1 resolution envelope'
1130
1224
  puts ' --dry-run plan the scaffold; write no files'
1131
1225
  exit 1
1132
1226
  end
@@ -1164,6 +1258,10 @@ module Tina4
1164
1258
  end
1165
1259
 
1166
1260
  resolution = build_generate_resolution(what, name, flags)
1261
+ # ADR-0063 additive v1.1 keys: curated next-steps (pure fn of resolution)
1262
+ # and edit_hints (scan of generated files for `# tina4:edit <label>`).
1263
+ resolution["next"] = build_next_steps(what, resolution)
1264
+ resolution["edit_hints"] = collect_edit_hints_via_sandbox(gen_spec, name, flags)
1167
1265
 
1168
1266
  if json_mode
1169
1267
  actions_taken = dry_run ? [] : run_generator_capturing_actions(gen_spec, name, flags)
@@ -1183,6 +1281,78 @@ module Tina4
1183
1281
  end
1184
1282
  end
1185
1283
 
1284
+ # ADR-0063 helpers ────────────────────────────────────────────────────────
1285
+
1286
+ # Curated per-verb next-step list. Pure function of the resolution; never
1287
+ # scans the filesystem, so it works identically in dry-run and real-run.
1288
+ def build_next_steps(target, resolution)
1289
+ builder = NEXT_STEPS[target]
1290
+ return [] unless builder
1291
+ Array(builder.call(resolution))
1292
+ end
1293
+
1294
+ # Scan a list of files for `# tina4:edit <label>` (Ruby) or
1295
+ # `-- tina4:edit <label>` (SQL) lines. Returns `[{file, line, label}]` in
1296
+ # the same order the files were passed. Missing / unreadable files are
1297
+ # skipped silently (a scaffold that already exists on disk simply produces
1298
+ # no hint for that path — that is the intended behaviour).
1299
+ def scan_edit_hints_from_files(paths)
1300
+ hints = []
1301
+ paths.each do |path|
1302
+ next unless path
1303
+ next unless File.file?(path)
1304
+ begin
1305
+ content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
1306
+ rescue SystemCallError
1307
+ next
1308
+ end
1309
+ content.each_line.with_index(1) do |line, lineno|
1310
+ next unless (m = line.match(EDIT_MARKER_RE))
1311
+ hints << { "file" => path, "line" => lineno, "label" => m[1].strip }
1312
+ end
1313
+ end
1314
+ hints
1315
+ end
1316
+
1317
+ # Run the generator inside a throw-away tmpdir with `$stdout` captured, then
1318
+ # walk every written file and scan for `# tina4:edit` / `-- tina4:edit`
1319
+ # markers. Used for BOTH dry-run and real-run modes so the envelope always
1320
+ # ships accurate `edit_hints` before any live-cwd write happens (preserves
1321
+ # the "Ctrl-C between plan and write" contract of the human block).
1322
+ #
1323
+ # `generate_timestamp` is shared with the real run so a migration file
1324
+ # sandboxed here has the SAME timestamped basename as the file that lands
1325
+ # in the developer's cwd - the hint's `file` field matches disk verbatim.
1326
+ def collect_edit_hints_via_sandbox(gen_spec, name, flags)
1327
+ hints = []
1328
+ Dir.mktmpdir("tina4_edit_hints") do |sandbox|
1329
+ # Block-form Dir.chdir composes cleanly under an outer block-form
1330
+ # Dir.chdir (Ruby 3.x warns on nested non-block chdir). No `ensure`
1331
+ # cwd-restore needed - the block form guarantees it.
1332
+ Dir.chdir(sandbox) do
1333
+ buffer = StringIO.new
1334
+ original_stdout = $stdout
1335
+ $stdout = buffer
1336
+ begin
1337
+ # A generator failure inside the sandbox must never break the caller's
1338
+ # real run - rescue everything, return whatever hints we managed to
1339
+ # gather. The real run will error the same way if it is going to.
1340
+ send(gen_spec[:handler], name, flags.dup)
1341
+ rescue StandardError, LoadError, ScriptError
1342
+ # swallow - the sandbox is best-effort
1343
+ ensure
1344
+ $stdout = original_stdout
1345
+ end
1346
+ Dir.glob("**/*", File::FNM_DOTMATCH).sort.each do |rel_path|
1347
+ next if [".", ".."].include?(File.basename(rel_path))
1348
+ next unless File.file?(rel_path)
1349
+ hints.concat(scan_edit_hints_from_files([rel_path]))
1350
+ end
1351
+ end
1352
+ end
1353
+ hints
1354
+ end
1355
+
1186
1356
  # ── generate resolution helpers ────────────────────────────────────────
1187
1357
 
1188
1358
  # One `resolution` per target, with the SAME KEY SET so a caller can
@@ -1227,7 +1397,10 @@ module Tina4
1227
1397
  # The migration filename embeds a timestamp; predict it deterministically
1228
1398
  # so a `--dry-run` and the real run agree on the same second. Two
1229
1399
  # generations within the same second collide anyway (existing behaviour).
1230
- timestamp = Time.now.strftime("%Y%m%d%H%M%S")
1400
+ # ADR-0063: `generate_timestamp` memoises `Time.now` per `cmd_generate`
1401
+ # call so the sandbox pre-run + real run share one second, and the
1402
+ # envelope's `migration_path` matches the file that actually lands.
1403
+ timestamp = generate_timestamp.strftime("%Y%m%d%H%M%S")
1231
1404
  {
1232
1405
  "class_name" => name.to_s,
1233
1406
  "table_name" => table,
@@ -1253,7 +1426,8 @@ module Tina4
1253
1426
  end
1254
1427
 
1255
1428
  def build_migration_resolution(name, _flags)
1256
- timestamp = Time.now.strftime("%Y%m%d%H%M%S")
1429
+ # ADR-0063: shared memoised timestamp (see `generate_timestamp`).
1430
+ timestamp = generate_timestamp.strftime("%Y%m%d%H%M%S")
1257
1431
  base = name.to_s.sub(/^create_/, "").sub(/^add_/, "").sub(/^drop_/, "")
1258
1432
  snake_base = to_snake_case(base)
1259
1433
  path = "migrations/#{timestamp}_#{name}.sql"
@@ -1313,12 +1487,33 @@ module Tina4
1313
1487
  if resolution["migration_path"] && resolution["migration_path"] != resolution["file_path"]
1314
1488
  lines << " migration #{resolution['migration_path']}"
1315
1489
  end
1490
+ # ADR-0063: surface the pre-existing test_paths (v1 field, only now printed
1491
+ # in the human block) alongside the two new sections.
1492
+ if resolution["test_paths"] && !resolution["test_paths"].empty?
1493
+ lines << " tests #{resolution['test_paths'].join(', ')}"
1494
+ end
1316
1495
  pluralized = resolution["transformations"].find { |t| t["kind"] == "reserved_word_pluralize" }
1317
1496
  if pluralized
1318
1497
  lines << ""
1319
1498
  lines << " To keep the raw name '#{pluralized['from']}' as the table:"
1320
1499
  lines << " tina4ruby generate #{target} #{name} --table #{pluralized['from']} --quote (opt-in, ADR-0062 forthcoming)"
1321
1500
  end
1501
+ # ADR-0063 v1.1: surface `edit_hints[]` under "Edit these lines:" and
1502
+ # `next[]` under "Next:" — both additive; missing / empty arrays elide
1503
+ # cleanly. The `file:line label` shape is grep-friendly for humans and
1504
+ # machine-parseable for tools.
1505
+ if resolution["edit_hints"] && !resolution["edit_hints"].empty?
1506
+ lines << ""
1507
+ lines << " Edit these lines:"
1508
+ resolution["edit_hints"].each do |h|
1509
+ lines << " #{h['file']}:#{h['line']} #{h['label']}"
1510
+ end
1511
+ end
1512
+ if resolution["next"] && !resolution["next"].empty?
1513
+ lines << ""
1514
+ lines << " Next:"
1515
+ resolution["next"].each { |step| lines << " #{step}" }
1516
+ end
1322
1517
  lines.join("\n")
1323
1518
  end
1324
1519
 
@@ -1371,6 +1566,7 @@ module Tina4
1371
1566
  class #{name} < Tina4::ORM
1372
1567
  table_name "#{table}"
1373
1568
 
1569
+ # tina4:edit add fields beyond the default 'name'
1374
1570
  #{field_lines.join("\n")}
1375
1571
  end
1376
1572
  RUBY
@@ -1628,7 +1824,10 @@ module Tina4
1628
1824
  # ── Generator: migration ─────────────────────────────────────────────
1629
1825
 
1630
1826
  def generate_migration(name, flags = {}, fields_override: nil, table_override: nil, emit_test: true)
1631
- now = Time.now
1827
+ # ADR-0063: `generate_timestamp` is the shared memoised `Time.now` for
1828
+ # this cmd_generate run; the resolution's `migration_path` uses the SAME
1829
+ # value, so envelope and disk agree.
1830
+ now = generate_timestamp
1632
1831
  timestamp = now.strftime("%Y%m%d%H%M%S")
1633
1832
  dir = "migrations"
1634
1833
  FileUtils.mkdir_p(dir)
@@ -1661,11 +1860,18 @@ module Tina4
1661
1860
  end
1662
1861
  col_lines << " created_at TEXT DEFAULT CURRENT_TIMESTAMP"
1663
1862
 
1664
- up_sql = "CREATE TABLE IF NOT EXISTS #{table} (\n#{col_lines.join(",\n")}\n);"
1665
- down_sql = "DROP TABLE IF EXISTS #{table};"
1863
+ # ADR-0063: `-- tina4:edit` marker for the SQL scanner (mirrors `# tina4:edit`
1864
+ # in Ruby files). The scanner regex accepts both comment prefixes.
1865
+ up_sql = "CREATE TABLE IF NOT EXISTS #{table} (\n" \
1866
+ " -- tina4:edit add columns beyond id + created_at\n" \
1867
+ "#{col_lines.join(",\n")}\n);"
1868
+ down_sql = "-- tina4:edit mirror the CREATE's added columns in the rollback\n" \
1869
+ "DROP TABLE IF EXISTS #{table};"
1666
1870
  else
1667
- up_sql = "-- Write your UP migration SQL here\n-- Example: ALTER TABLE #{table} ADD COLUMN new_col TEXT DEFAULT '';"
1668
- down_sql = "-- Write your DOWN rollback SQL here\n-- Example: ALTER TABLE #{table} DROP COLUMN new_col;"
1871
+ up_sql = "-- tina4:edit write your UP migration SQL here\n" \
1872
+ "-- Example: ALTER TABLE #{table} ADD COLUMN new_col TEXT DEFAULT '';"
1873
+ down_sql = "-- tina4:edit write your DOWN rollback SQL here\n" \
1874
+ "-- Example: ALTER TABLE #{table} DROP COLUMN new_col;"
1669
1875
  end
1670
1876
 
1671
1877
  # The main .sql holds ONLY the UP migration. The runner executes the WHOLE
@@ -1727,12 +1933,14 @@ module Tina4
1727
1933
  # Runs before the route handler.
1728
1934
  # Return [request, response] to continue, or
1729
1935
  # return [request, response.json({ error: "Unauthorized" }, 401)] to block.
1936
+ # tina4:edit guard the request here
1730
1937
  Tina4::Log.info("#{name}: \#{request.method} \#{request.path}")
1731
1938
  [request, response]
1732
1939
  end
1733
1940
 
1734
1941
  def self.after_#{snake}(request, response)
1735
1942
  # Runs after the route handler.
1943
+ # tina4:edit post-process the response here
1736
1944
  [request, response]
1737
1945
  end
1738
1946
  end
@@ -1836,6 +2044,7 @@ module Tina4
1836
2044
  # Tests for #{name} CRUD operations
1837
2045
  RSpec.describe "#{model}" do
1838
2046
  before(:each) do
2047
+ # tina4:edit seed real fixtures for a real DB
1839
2048
  # Set up test fixtures
1840
2049
  end
1841
2050
 
@@ -1844,7 +2053,7 @@ module Tina4
1844
2053
  end
1845
2054
 
1846
2055
  it "lists #{snake}" do
1847
- # TODO: implement
2056
+ # tina4:edit replace the tautology with a real assertion
1848
2057
  expect(true).to be true
1849
2058
  end
1850
2059
 
@@ -1875,6 +2084,7 @@ module Tina4
1875
2084
  # Tests for #{name}
1876
2085
  RSpec.describe "#{class_name}" do
1877
2086
  before(:each) do
2087
+ # tina4:edit seed real fixtures for a real DB
1878
2088
  # Set up test fixtures
1879
2089
  end
1880
2090
 
@@ -1883,7 +2093,7 @@ module Tina4
1883
2093
  end
1884
2094
 
1885
2095
  it "works as expected" do
1886
- # TODO: replace with real tests
2096
+ # tina4:edit replace the tautology with a real assertion
1887
2097
  expect(true).to be true
1888
2098
  end
1889
2099
  end
@@ -1955,6 +2165,7 @@ module Tina4
1955
2165
  end
1956
2166
 
1957
2167
  content = <<~HTML
2168
+ {# tina4:edit restyle the form beyond the scaffolded defaults #}
1958
2169
  {% extends "base.twig" %}
1959
2170
  {% block title %}#{name} {% if item.id %}Edit{% else %}Create{% endif %}{% endblock %}
1960
2171
  {% block content %}
@@ -1994,6 +2205,7 @@ module Tina4
1994
2205
  td = cols.map { |c| "<td>{{ item.#{c} }}</td>" }.join("\n ")
1995
2206
 
1996
2207
  list_content = <<~HTML
2208
+ {# tina4:edit add sort / filter / pagination controls to the list #}
1997
2209
  {% extends "base.twig" %}
1998
2210
  {% block title %}#{name}s{% endblock %}
1999
2211
  {% block content %}
@@ -2044,6 +2256,7 @@ module Tina4
2044
2256
  end.join("\n")
2045
2257
 
2046
2258
  detail_content = <<~HTML
2259
+ {# tina4:edit extend the detail view with related records or actions #}
2047
2260
  {% extends "base.twig" %}
2048
2261
  {% block title %}#{name} Detail{% endblock %}
2049
2262
  {% block content %}
@@ -2087,6 +2300,7 @@ module Tina4
2087
2300
  # clears the router's auth_required).
2088
2301
  Tina4.post "/api/auth/register", auth: false do |request, response|
2089
2302
  # Register a new user
2303
+ # tina4:edit harden the register flow (rate limit / captcha / password rules)
2090
2304
  email = request.body["email"].to_s
2091
2305
  password = request.body["password"].to_s
2092
2306
 
@@ -2112,6 +2326,7 @@ module Tina4
2112
2326
  # PUBLIC: login mints the token — clear BOTH write gates (see register).
2113
2327
  Tina4.post "/api/auth/login", auth: false do |request, response|
2114
2328
  # Login with email and password
2329
+ # tina4:edit harden the login flow (rate limit / lockout after N failures)
2115
2330
  email = request.body["email"].to_s
2116
2331
  password = request.body["password"].to_s
2117
2332
 
@@ -3146,7 +3361,12 @@ module Tina4
3146
3361
  # KNOW the envelope shape before it invokes generate (see cmd_generate).
3147
3362
  # Bump `version` when the envelope shape changes; add a new envelope
3148
3363
  # name rather than mutating the existing one.
3149
- "resolution_contract" => { "version" => "1", "envelope" => "generate_v1" }
3364
+ # ADR-0063: v1.1 adds two additive keys to `resolution` — `edit_hints[]`
3365
+ # (grep of every `# tina4:edit <label>` / `-- tina4:edit <label>` line
3366
+ # in the generated files) and `next[]` (curated actionable steps). Every
3367
+ # v1 key is preserved untouched, so a v1 caller reading v1.1 output
3368
+ # keeps working.
3369
+ "resolution_contract" => { "version" => "1.1", "envelope" => "generate_v1_1" }
3150
3370
  }
3151
3371
  end
3152
3372
 
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.118"
4
+ VERSION = "3.13.120"
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.118
4
+ version: 3.13.120
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-08-26 00:00:00.000000000 Z
11
+ date: 2026-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack