Ir al contenido principal

Ralsina.Me — El sitio web de Roberto Alsina

New project: ToCry, a ToDo/Kanban app

I have been us­ing Tasks.md for a while now and ... I like it, but I got the itch to try fix­ing some things and I did­n't re­al­ly want to do JS back­end code on week­end­s, so why not try to build a sim­i­lar ap­p?

Al­so I have been want­ing to do some "vibe cod­ing" with AI and I seem to have un­lim­it­ed Gem­i­ni 2.5 in VS Code for some rea­son, so why not try to do that too?

Well: it worked, you can see the re­sult at ToCry, get the app and use it. It's pret­ty nice!

alt text

Now, I am not say­ing it's per­fec­t, but it works and I learned a lot. Did AI help? Well, yeah. Most of the fron­tend code was AI gen­er­at­ed, most of the back­end code is mine with AI au­to­com­plet­ing stuff.

And this got writ­ten pret­ty fast, I on­ly start­ed it two days ago!

In fac­t, in those two days it was writ­ten twice... be­cause the first time it was ab­so­lute garbage.

It was so bad I not on­ly rewrote it, I re­moved the re­po his­to­ry and pushed the new code over it. I did­n't want to keep that code near, I did not want it to in­flu­ence the re­write.

What Went Wrong The First Time

The first time I went UI-­first. I just prompt­ed and prompt­ed and prompt­ed on a HTML file, try­ing to get Gem­i­ni to gen­er­ate a nice UI that worked with all the da­ta kept on the client side.

This seemed to work well, in a few hours I had a nice UI that was at least func­tion­al. How­ev­er at one point Gem­i­ni hit a wal­l, hard.

Try­ing to add a new fea­ture or fix­ing a bug was al­most im­pos­si­ble, Gem­i­ni would get in loop af­ter loop, do­ing and re­vert­ing the same changes over and over.

When I went to man­u­al­ly check the code, it was a bowl of spa­het­ti, and refac­tor­ing it was be­yond Gem­i­ni's ca­pa­bil­i­ties. Con­nect­ing it to a back­end was doable (and got done) but then state was kept in the client and the back­end, and Gem­i­ni re­fused to move it and con­sis­ten­cy was a night­mare.

It be­came in­creas­ing­ly clear that the code was of no val­ue. So then I nuked it, salt­ed the earth, and start­ed over.

What Went Right The Second Time

The sec­ond time I went back­end-­first. I wrote a sim­ple back­end in Crys­tal, with a sim­ple API that re­turned JSON da­ta for the en­ti­ties I want­ed to man­age: tasks, lanes, board­s, etc.

Af­ter the da­ta struc­tures were clear, prompts like "cre­ate a PUT /note/:id end­point that up­dates the note" worked great. The trick is, of course, that those re­quests are pret­ty much con­tex­t-free, so Gem­i­ni did­n't have to fig­ure out how to con­nect one thing to an­oth­er, it was just writ­ing al­most-boil­er­plate code.

Af­ter sev­er­al of these were cre­at­ed, I in­ter­min­gled refac­tor­ing prompt­s.

  • "Do you see any code that can be moved to a com­mon func­tion?"
  • "Please as­sume this and that are al­ways true and stop check­ing"
  • "Change the names of this and that to some­thing de­scrip­tive and short"

I still am not a huge fan of the code Gem­i­ni writes. It has a ten­den­cy to do mul­ti­ple in­ter­me­di­ate steps that are not need­ed and us­ing in­ter­me­di­ate vari­ables in some sort of over de­scrip­tive no­ta­tion all the time.

Ah, and adds stupid com­ments. Many stupid com­ments. This is an ex­am­ple of its code:

self.notes.each_with_index do |note, index| # Changed to each_with_index to get the index
        note.save                   # This saves the note to data/.notes/note_id.md

        padded_index = index.to_s.rjust(4, '0')
        sanitized_title = note.slug # Note instance method for slug

        symlink_filename = "#{padded_index}_#{sanitized_title}.md"
        source_note_path = File.join("..", ".notes", "#{note.id}.md") # Relative path for symlink
        symlink_target_path = File.join(lane_dir, symlink_filename)
        ...

Usu­al­ly, af­ter a func­tion is "fin­ished", I would go and do a pass "for taste" to make the code more read­able, re­move the un­nec­es­sary com­ments, and make it more con­cise, but the code is func­tion­al and this is CRUD, not a fash­ion show.

The same hap­pened with the fron­tend code. I prompt­ed slow, bit by bit:

  • Add a but­ton to cre­ate a new lane, so when the us­er clicks it they are asked for the lane name. Then it calls POST /lane with the right da­ta
  • Get the lane da­ta from /lanes and dis­play it as a hor­i­zon­tal list of con­tain­ers
  • Add a but­ton to the lane to delete it, which prompts the us­er for con­fir­ma­tion and then calls DELETE /lane/:id

And so on.

Some­times, I tried larg­er prompts where I asked for a whole fea­ture, but those were on­ly oc­ca­sion­al­ly suc­cess­ful. Find­ing the right gran­u­lar­i­ty is key, and I found that the best gran­u­lar­i­ty was "one func­tion" or "one com­po­nen­t".

Again, mix­ing refac­tor­ing as we went along helped a lot to keep the code clean and or­ga­nized, as well as sep­a­rat­ed in rea­son­ably func­tion­s, keep­ing spaghet­ti at bay.

What I Learned

Gem­i­ni codes like a ju­nior. It does not un­der­stand the big pic­ture, it does not un­der­stand the code it writes, It can write code that works ( I as­sume be­cause the In­ter­net is full of work­ing code) but it car­go cults AF.

On the oth­er hand, the code tends to work? And I can fix it pret­ty quick when it does­n't? It's not bad at sim­ple refac­tor­ing, and what it needs more than any­thing is a man­ag­er.

Yes, that seems to be the bad news. Cod­ing with AI felt like be­ing a man­ag­er again. A man­ag­er with a very, very, very ea­ger ju­nior dev who does­n't sleep and feels soooo clever by de­scrib­ing unini­tial­ized vari­ables as "a clas­sic ex­am­ple of a vari­able that has not been ini­tial­ized yet".

Would I Do It Again?

Yeah. I have a ton of things I want to write, and this way I can write them faster. I can al­so put ef­fort in the parts that mat­ter, like da­ta struc­tures and al­go­rithms and let Gem­i­ni do the sil­ly CSS and CRUD.

Yeah, I don't care if the CSS is re­dun­dan­t, as long as it looks ok I am hap­py telling Gem­i­ni to "make the rows tighter" and who cares how.

CRUD? It's a solved prob­lem. I will do a pass to clean up.

Da­ta struc­tures? I will do 90% of the work, be­cause that is what makes the base of the ap­p, and I want it to be sol­id.

Ethical Thoughts

I feel dirty, and like I am cheat­ing. I am prob­a­bly steal­ing code from oth­er peo­ple. Use Gem­i­ni to write a Dock­er­file and it will hap­pi­ly au­to­com­plete things with frag­ments of things in­clud­ing re­po names which ex­ist in the In­ter­net so it's not even a guess, I know it's copy past­ing oth­er peo­ple's code.

OTO­H, I al­ways copy past­ed code from oth­er peo­ple's code.

Writing a simple parser using a finite state machine

I wrote a lit­er­ate pro­gram­ming tool called Cryc­co and at its core is a very sim­ple pars­er which ex­tracts com­ment blocks from source code and or­ga­nizes them in­to a doc­u­men­t.

If you want to see an ex­am­ple, Cryc­co's web­site is made out of its own code by pro­cess­ing it with it­self.

The for­mat is very sim­ple: you write a com­ment block, then some code, then an­oth­er com­ment block, and so on. The com­ment blocks are tak­en as mark­down, and the code blocks are tak­en as source code.

The doc­u­ment is then split in sec­tion­s: 1 chunk of doc, 1 chunk of code, and pro­cessed to make it look nice, with syn­tax high­light­ing and so on.

The pars­er was just a cou­ple dozen lines of code, but when I want­ed to add more fea­tures I ran in­to a wal­l, so I rewrote it us­ing a fi­nite state ma­chine (F­S­M) ap­proach.

Since, again, Cryc­co is lit­er­ate code, you can see the pars­er by just look­ing at the source code of the pars­er it­self, which is in the Doc­u­ment class

Of course, that may not be enough, so let's go in­to de­tail­s.

The state ma­chine has a few states:

    enum State
      CommentBlock
      EnclosingCommentBlock
      CodeBlock
    end

A com­ment block is some­thing like this:

    # This is a comment
    # More comment

An en­clos­ing com­ment block is a "mul­ti­line" com­men­t, like this:

 /* This is a comment
 More comment
 */

Code blocks are lines that are not com­ments :-)

So, suppose you are in the CommentBlock state, and you see a line that starts with # you stay in the same state.

If you see a line that does not start with #, you switch to the CodeBlock state.

When you are in the CommentBlock state, the line you are in is a comment. If you are in the CodeBlock state, the line is code.

Here are the pos­si­ble tran­si­tions in this ma­chine:

    state_machine State, initial: State::CodeBlock do
      event :comment, from: [State::CodeBlock], to: State::CommentBlock
      event :enclosing_comment_start, from: [State::CodeBlock], to: State::EnclosingCommentBlock
      event :enclosing_comment_end, from: [State::EnclosingCommentBlock], to: State::CodeBlock
      event :code, from: [State::CommentBlock], to: State::CodeBlock
    end

Then, to parse the doc­u­men­t, we go line by line, and call the ap­pro­pri­ate event de­pend­ing on the line we are read­ing. That event may change the state or not.

For ex­am­ple:

        if is_comment.match(line) && !NOT_COMMENT.match(line)
          self.comment {
            # These blocks only execute when transitions are successful.
            #
            # So, this block is executed when we are transitioning
            # to a comment block, which means we are starting
            # a new section
            @sections << Section.new(@language)
          }
          # Because the docs section is supposed to be markdown, we need
          # to remove the comment marker from the line.
          processed_line = processed_line.sub(@language.match, "") unless @literate

And that's it! We send the prop­er events to the ma­chine, the ma­chine changes state, we han­dle the line ac­cord­ing to what state we are in, and we end up with a nice­ly parsed doc­u­men­t.

Parsers are some­what scary, but they don't have to be. A fi­nite state ma­chine is a very sim­ple way to write a pars­er that is easy to un­der­stand and main­tain, and of­ten is enough.

Have fun pars­ing!

Literate version of grafito code (WIP)

In the past cou­ple of weeks I have start­ed (and pret­ty much fin­ished) a tool called Grafi­to and the end re­sult is un­der two thou­sand lines of code, in­clud­ing HTML and CSS.

For small­er code­bas­es, I think it makes sense to make them lit­er­ate. Just a cou­ple hours writ­ing around the code can make it per­fect­ly clear to un­der­stand for any­one start­ing with the code­base and (to be hon­est) al­so for the me of the fu­ture who will re­mem­ber noth­ing about it.

So I am pub­lish­ing the com­ment­ed code­base of grafi­to pro­cessed through Cryc­co a lit­er­ate pro­gram­ming tool I wrote. Yes, the web­site for Cryc­co is Cryc­co's source code. That's tra­di­tion­al :-)

The code is not yet ful­ly com­ment­ed and I have found a cou­ple bugs in Cryc­co al­ready:

  • Links in the side­bar are wrong in some cas­es
  • There is no way to pub­lish a lit­er­ate HTML file!

In any case, I ex­pect no­body cares, but I think it's nice and it's not a ton of ef­fort so that makes it worth do­ing, so I did it.

Creating a demo site for a service

Re­cent­ly I wrote an app called Grafi­to to view sys­temd/jour­nald logs (those are the logs in most Lin­ux sys­tem­s) and be able to fil­ter them, see de­tails of a spe­cif­ic en­try, etc.

One prob­lem with this kind of tool is that I can't just open it to the world be­cause then ev­ery­one would be able to see the logs in a re­al ma­chine. While that is usu­al­ly not a prob­lem be­cause the in­for­ma­tion is not ter­ri­bly use­ful (sure, you will know what's run­ning, big whoop­s), it may dis­play a dan­ger­ous piece of da­ta which I may not want to ex­pose.

So, the right way to do this is to cre­ate a de­mo site. It could be show­ing the re­al da­ta from a throw­away sys­tem (like a vir­tu­al ma­chine) or like I did show fake da­ta.

To show fake da­ta you can use a fak­er. Fak­ers are fun! I am us­ing askn/­fak­er which is a Crys­tal one. Fak­ers let you ask for, you guessed it... fake da­ta.

For ex­am­ple, you can ask for an ad­dress or a cred­it card num­ber and it would give you some­thing ran­dom that match­es the ob­vi­ous pat­terns of what you ask.

One I love is to ask for say_something_smart which gives you smart things!

Faker::Hacker.say_something_smart #=> 
"Try to compress the SQL interface, maybe it will program the 
back-end hard drive!"

So, I wrote a function that works like journalctl but is totally fake. The source code is just a quick hack.

Then, I used a con­di­tion­al com­pile flag to route the in­fo re­quests in­to that fake func­tion:

{% if flag?(:fake_journal) %}
  require "./fake_journal_data" # For fake data generation
{% end %}

{% if flag?(:fake_journal) %}
    Log.info { "Journalctl.known_service_units: Using FAKE service units." }
    fake_units = FakeJournalData::SAMPLE_UNIT_NAMES.compact.uniq.sort
    Log.debug { "Returning #{fake_units.size} fake service units." }
    return fake_units
{% else %}
    # Return actual good stuff
{% end %}

And that's it! If I compile with -Dfake_journal it builds a binary that is using only fake data. Then I had it run in my home server and voilá: a demo!

See it in ac­tion! grafi­to-de­mo.ralsi­na.me


Contents © 2000-2025 Roberto Alsina