<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Georgios Theodorakopoulos - TalentLMS Tech Blog</title><link>https://blog.talentlms.io/authors/gtheodorakopoulos/</link><description>Posts by Georgios Theodorakopoulos - Insights and stories from the TalentLMS engineering team. Technical deep-dives, team culture, and lessons learned building learning management software.</description><language>en-US</language><managingEditor>noreply@talentlms.com (TalentLMS Engineering Team)</managingEditor><webMaster>noreply@talentlms.com (TalentLMS Engineering Team)</webMaster><lastBuildDate>Mon, 20 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.talentlms.io/authors/gtheodorakopoulos/feed.xml" rel="self" type="application/rss+xml"/><image><url>https://blog.talentlms.io/images/logo/logo.svg</url><title>TalentLMS Tech Blog</title><link>https://blog.talentlms.io/</link></image><item><title>Leveraging Cursor in a Large-Scale Project: My First Experience</title><link>https://blog.talentlms.io/posts/leveraging-cursor-in-a-large-scale-project/</link><pubDate>Mon, 20 Apr 2026 00:00:00 +0000</pubDate><dc:creator>Georgios Theodorakopoulos</dc:creator><guid>https://blog.talentlms.io/posts/leveraging-cursor-in-a-large-scale-project/</guid><description>Onboarding onto a large LMS codebase, I used Cursor not to write features faster, but to build a mental model: where legacy PHP meets newer REST layers, how events propagate, and where permission checks actually live.
This post walks through two real explorations (user impersonation across stacks and a permissions trace), with anonymized prompts, what the tool got right and wrong, and a small playbook you can reuse on your own brownfield project.</description><enclosure url="https://blog.talentlms.io/images/posts/leveraging-cursor-in-a-large-scale-project.png" type="image/png"/><media:content url="https://blog.talentlms.io/images/posts/leveraging-cursor-in-a-large-scale-project.png" medium="image"><media:title type="plain">Abstract painted figure before a branching network of nodes and pathways on a warm background</media:title><media:description type="plain">Abstract painted figure before a branching network of nodes and pathways on a warm background</media:description></media:content><content:encoded><![CDATA[<img src="https://blog.talentlms.io/images/posts/leveraging-cursor-in-a-large-scale-project.png" alt="Abstract painted figure before a branching network of nodes and pathways on a warm background" style="max-width: 100%; height: auto; margin-bottom: 1.5em;" /><p style="margin-bottom: 1.5em; padding: 1em; background-color: #f5f5f5; border-left: 4px solid #0066cc;"><strong>Georgios Theodorakopoulos</strong>, Software Engineer<br/>George’s journey into backend development started at the age of 11, when he first encountered the MySQL dolphin
while trying to launch his very first modded Minecraft server. After many failed setups, …</p><p>When I started at Epignosis, I folded <a href="https://cursor.com/" target="_blank" rel="noopener noreferrer">Cursor</a> into how I actually work, not as a novelty, but as something I reached for when the codebase outpaced my notes. It was the first time I relied on an AI editor for more than autocomplete in a real, high-complexity environment. What changed was not my typing speed; it was how I budget time between discovery and implementation.</p>
<h2 id="the-context">The Context</h2>
<p>Every codebase has a personality. History baked into it. Mine arrived as a large LMS: many modules, wires between them, and legacy paths you do not refactor for sport. Onboarding here is not “read the README and ship.” It is learning where the same word means different things depending on which folder you are in.</p>
<h2 id="first-impressions">First Impressions</h2>
<p>Cursor felt less like an autocomplete box and more like a second pair of eyes that could keep several files in view at once. The part that mattered for this post is not the suggestions themselves. It is that I could ask in plain language instead of chaining <code>grep</code>, IDE search, and hallway questions, and still know I had to verify everything myself. The rest of this article is about that balance.</p>
<h2 id="my-first-task-an-event-in-an-event-driven-flow">My First Task: An Event in an Event-Driven Flow</h2>
<p>My first assignment was to implement a new <strong>EDA</strong> (event-driven architecture) event: a message the system emits when something important happens, with consumers in other services or layers.</p>
<p>The questions piled up fast:</p>
<ul>
<li>Where do handlers for similar events live?</li>
<li>How does the legacy stack publish or consume events compared to the newer REST API?</li>
<li>What breaks if the payload shape is wrong?</li>
</ul>
<p>Here is where I stopped treating all search tools as interchangeable.</p>
<p><strong><code>grep</code> / ripgrep</strong> is unbeatable when you already know the string: a class name, a queue name, a constant. You get a list of hits, you open files, you stitch the story together yourself. It is honest work. It breaks down when the codebase uses five phrases for the same idea, or when the important word is buried in a string builder three calls deep.</p>
<p><strong>IDE search</strong> (scoped to a folder, or “find usages” on a symbol) widens the net. It is still fundamentally <em>text and symbols</em>. You can filter by path and file type, which helps in a monorepo, but you are still guessing which symbol to anchor on. If you pick the wrong entry point, you spend an afternoon in the wrong neighborhood.</p>
<p><strong>Cursor with a narrow mission</strong> was different in kind, not only in degree. I could ask for “where events like this are published <em>and</em> consumed,” across legacy and newer layers, and get a <em>proposed</em> map: files grouped by role, sometimes synonyms I would not have thought to search. That map was often wrong in the details. It was still a faster wrong than a slow blind search because I could correct it with the debugger and a close read, instead of discovering I had been searching the wrong word at hour three.</p>
<p>None of that replaces <code>grep</code>. I still use it every day. The point is to match the tool to the uncertainty: exact string → ripgrep; symbol you trust → IDE; fuzzy concept spanning stacks → assistant first, then verify.</p>
<p>A pattern I was looking for in existing handlers looked conceptually like this (dummy names and schema; only the shape matters):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;event&#34;</span>: <span style="color:#e6db74">&#34;domain.course.created&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;occurred_at&#34;</span>: <span style="color:#e6db74">&#34;2026-03-30T12:00:00Z&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;tenant_id&#34;</span>: <span style="color:#e6db74">&#34;acme&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;payload&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;course_id&#34;</span>: <span style="color:#e6db74">&#34;crs_01jqxyz&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;actor_user_id&#34;</span>: <span style="color:#e6db74">&#34;usr_01abc&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">&lt;?</span><span style="color:#a6e22e">php</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Illustrative only, not production code
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">final</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">CourseCreatedPublisher</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">publish</span>(<span style="color:#a6e22e">CourseCreated</span> $event, <span style="color:#a6e22e">EventBus</span> $bus)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        $envelope <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">envelopeFactory</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">fromDomainEvent</span>($event);
</span></span><span style="display:flex;"><span>        $bus<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">dispatch</span>(<span style="color:#e6db74">&#39;domain.course.created&#39;</span>, $envelope);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Technically, what I cared about next was not “emit an event” in the happy path. It was <strong>contract compatibility</strong>: <code>version</code> in the envelope, whether consumers are <strong>at-least-once</strong> and therefore need <strong>idempotency keys</strong>, and what happens when one subscriber upgrades before another. I will say it plainly: <strong>event schemas are public APIs</strong>. Treat them as an afterthought and you will ship a breaking change that only shows up under load or in a downstream queue. Cursor helped me find where similar envelopes were assembled and validated; it did not write the idempotency strategy for me.</p>
<p>There was no single obvious entry point. I spent half a day in internal docs and file trees, useful but fragmented. That is when I switched from “read everything” to “tell the assistant what success looks like, then tear the answer apart,” with the same verification I would use after any senior’s sketch on a whiteboard.</p>
<h2 id="from-vague-prompts-to-missions">From Vague Prompts to Missions</h2>
<p>I started with a broad prompt:</p>
<blockquote>
<p><em>Hi, what can you tell me about this project?</em></p>
</blockquote>
<p>The answer was a reasonable map: major areas, how pieces relate. Fine for day one. It also showed me a rule I still use: <strong>vague questions get survey answers.</strong> They do not replace a goal.</p>
<p>So I reframed. Instead of “tell me about X,” I gave a mission with boundaries, stacks, folders, what “done” looks like. The next prompt looked like this:</p>
<blockquote>
<p><em>Find every place user impersonation is implemented or checked. Include legacy PHP modules and the newer REST API code. List files and how they connect.</em></p>
</blockquote>
<p>That shift, from open chat to scoped reconnaissance, is what made the tool feel earned instead of magical.</p>
<h2 id="case-study-1-impersonation-across-legacy-and-rest">Case Study 1: Impersonation Across Legacy and REST</h2>
<p>I kept the mission concrete: both <strong>legacy</strong> and <strong>REST API</strong> folders, and the <strong>interaction points</strong> between them, not a single happy path.</p>
<p>What came back was not a wall of prose. It read more like a <strong>reconnaissance brief</strong>: a short list of areas, then files grouped by role. In my own notes I distilled it into something like the structure below (names and paths are illustrative, not a copy-paste from our repo):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>legacy/
</span></span><span style="display:flex;"><span>  └── User/
</span></span><span style="display:flex;"><span>      └── Impersonation*.php          # session / context switch
</span></span><span style="display:flex;"><span>rest-api/
</span></span><span style="display:flex;"><span>  └── src/
</span></span><span style="display:flex;"><span>      └── Identity/
</span></span><span style="display:flex;"><span>          └── ImpersonationGuard.php  # token + permission gate
</span></span></code></pre></div><p>Alongside that, it pointed to middleware or filters that attach identity to the request, the same layers I would have had to discover by stepping through with a debugger.</p>
<p>On the REST side, a simplified version of what I expected to find (and later verified line-by-line) looked like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">&lt;?</span><span style="color:#a6e22e">php</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Illustrative middleware, real code has more guards
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">namespace</span> <span style="color:#a6e22e">App\Http\Middleware</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">final</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">AttachImpersonationContext</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">handle</span>(<span style="color:#a6e22e">Request</span> $request, <span style="color:#a6e22e">Closure</span> $next)<span style="color:#f92672">:</span> <span style="color:#a6e22e">Response</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        $token <span style="color:#f92672">=</span> $request<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">attributes</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#39;session_token&#39;</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> ($token<span style="color:#f92672">?-&gt;</span><span style="color:#a6e22e">isImpersonating</span>()) {
</span></span><span style="display:flex;"><span>            $request<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">attributes</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">set</span>(<span style="color:#e6db74">&#39;effective_user_id&#39;</span>, $token<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">subjectId</span>());
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $next($request);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>And a fragment of legacy-style session switching might resemble:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">&lt;?</span><span style="color:#a6e22e">php</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Illustrative legacy helper
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">tlms_begin_impersonation</span>(<span style="color:#a6e22e">int</span> $adminId, <span style="color:#a6e22e">int</span> $targetUserId)<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    $_SESSION[<span style="color:#e6db74">&#39;impersonation&#39;</span>] <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;admin_id&#39;</span> <span style="color:#f92672">=&gt;</span> $adminId,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;target_id&#39;</span> <span style="color:#f92672">=&gt;</span> $targetUserId,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;started_at&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">time</span>(),
</span></span><span style="display:flex;"><span>    ];
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Seeing both shapes in the same exploration session made it obvious where parity checks belong.</p>
<h3 id="how-i-verified-it">How I verified it</h3>
<p>Cursor gave me a <strong>hypothesis graph</strong>. I still:</p>
<ol>
<li>Opened each suggested file and read the real control flow.</li>
<li>Set a breakpoint on the REST path and walked the same route in XDebug.</li>
<li>Compared: does legacy enforce the same invariants as the new API, or only some of them?</li>
</ol>
<p>In one case the overview was slightly <strong>over-merged</strong>: two similarly named helpers were described as one flow when they served different entry points. That is not a reason to abandon the tool; it is a reason to treat its map as <strong>R&amp;D output</strong>, not a spec.</p>
<p>Roughly, that first targeted round took on the order of <strong>minutes</strong> to produce a navigable list; doing the same with search keywords alone would have been <strong>hours</strong> of false positives and missed synonyms (“impersonate” vs “act as” vs “switch user”).</p>
<p>From a security angle, impersonation is where I am least willing to trust generated code. I want <strong>explicit invariants</strong>: who initiated the switch, whether it is time-bounded, whether audit logs record both identities, and whether APIs reject confused-deputy patterns. My view is that an assistant is fine for <strong>locating</strong> those invariants across stacks; it is not the authority on whether your threat model is complete. If the map says “middleware X,” I still read X and ask whether that is sufficient for every transport (browser session, API token, background job). That skepticism is not cynicism about the tool; it is how I sleep at night.</p>
<h2 id="case-study-2-when-permissions-means-five-different-things">Case Study 2: When “Permissions” Means Five Different Things</h2>
<p>While tracing features, I kept hitting the word <strong>Permissions</strong>. In a smaller codebase that might be one module. Here it could mean:</p>
<table>
  <thead>
      <tr>
          <th>Layer</th>
          <th>What “permission” often refers to</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>HTTP API</td>
          <td>Route or scope checks on specific endpoints</td>
      </tr>
      <tr>
          <td>Domain / service</td>
          <td>Business rules (“may this user perform this action on this resource”)</td>
      </tr>
      <tr>
          <td>RBAC</td>
          <td>Roles and role-to-capability mapping</td>
      </tr>
      <tr>
          <td>Product / feature flags</td>
          <td>Gates that are not strictly authorization</td>
      </tr>
      <tr>
          <td>Infra</td>
          <td>Keys, environment, deployment not user auth</td>
      </tr>
  </tbody>
</table>
<p>After <strong>company onboarding</strong> (product and engineering orientation, not a public certification name), I could name concrete actions “create user,” “update user,” “view user” but I still did not know <strong>where</strong> those checks were enforced relative to “create course,” which spans UI, legacy API, and newer flows.</p>
<p>So I asked:</p>
<blockquote>
<p><em>Where is permission enforced so a user cannot create a course? Distinguish UI-only checks from API enforcement, and legacy vs newer paths.</em></p>
</blockquote>
<h3 id="what-made-this-answer-useful">What made this answer useful</h3>
<p>The valuable part was not “here is a file.” It was <strong>layering</strong>: legacy UI affordances, legacy API handlers, and REST handlers, with an explicit call-out where behavior could diverge, for example UI hiding a button while an API still allows the operation if called directly. That is the class of bug you hunt when you care about consistency, not just about compiling.</p>
<p>Dummy examples of what “three layers” can look like in practice:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// Client may hide UI without enforcing server-side (illustrative)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">canCreateCourse</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">usePermission</span>(<span style="color:#e6db74">&#39;courses.create&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</span> <span style="color:#a6e22e">canCreateCourse</span> <span style="color:#f92672">?</span> &lt;<span style="color:#f92672">CreateCourseButton</span> /&gt; <span style="color:#f92672">:</span> <span style="color:#66d9ef">null</span>;
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">&lt;?</span><span style="color:#a6e22e">php</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Legacy API handler illustrative
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">postCreateCourse</span>(<span style="color:#a6e22e">CreateCourseRequest</span> $req)<span style="color:#f92672">:</span> <span style="color:#a6e22e">JsonResponse</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span>$this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">acl</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">userMay</span>($req<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">user</span>(), <span style="color:#e6db74">&#39;courses.create&#39;</span>)) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">response</span>()<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">json</span>([<span style="color:#e6db74">&#39;error&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;forbidden&#39;</span>], <span style="color:#ae81ff">403</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">&lt;?</span><span style="color:#a6e22e">php</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// REST policy illustrative
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">final</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">CreateCoursePolicy</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">create</span>(<span style="color:#a6e22e">User</span> $actor, <span style="color:#a6e22e">Tenant</span> $tenant)<span style="color:#f92672">:</span> <span style="color:#a6e22e">bool</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">capabilities</span><span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">granted</span>($actor, $tenant, <span style="color:#e6db74">&#39;courses.create&#39;</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The point of the exercise was not to memorize snippets like these. It was to know <strong>which of them actually runs</strong> for the client I cared about.</p>
<p>I am opinionated about <strong>defense in depth</strong>: the UI should reflect policy, but the server must enforce it. If those two disagree, I consider it a defect unless there is a documented, intentional reason (for example, progressive enhancement with a degraded mode, which still needs a story for direct API access). In a brownfield LMS, “permission” often leaks into feature flags and product experiments too. I do not think those should be conflated with RBAC in code, even when marketing uses one word for all of them. Naming and module boundaries matter because the next engineer will grep for <code>permission</code> and land in the wrong layer.</p>
<h2 id="trade-offs-cursor-vs-other-tools">Trade-Offs: Cursor vs Other Tools</h2>
<p>None of these replace the others; they have different failure modes:</p>
<table>
  <thead>
      <tr>
          <th>Approach</th>
          <th>Strength</th>
          <th>Weakness</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>grep</code> / ripgrep</td>
          <td>Exact symbol search, fast</td>
          <td>Synonyms and indirect calls; no narrative</td>
      </tr>
      <tr>
          <td>IDE “Find usages”</td>
          <td>Refinement</td>
          <td>Noise in huge codebases; misses dynamic dispatch</td>
      </tr>
      <tr>
          <td>Debugger</td>
          <td>Ground truth for one execution</td>
          <td>Slow to cover all branches</td>
      </tr>
      <tr>
          <td>Cursor (directed prompts)</td>
          <td>Cross-file story and synonyms</td>
          <td>Can over-merge or hallucinate edge paths</td>
      </tr>
  </tbody>
</table>
<p>The workflow that worked for me: <strong>Cursor for a structured first pass, then the debugger and raw reading for proof.</strong> <a href="https://docs.cursor.com/" target="_blank" rel="noopener noreferrer">Cursor’s own documentation</a> stresses context and rules; pairing that with repo-specific rules files (when your team maintains them) improves consistency.</p>
<h3 id="opinions-i-am-willing-to-defend">Opinions I am willing to defend</h3>
<ul>
<li><strong>Navigation beats codegen for onboarding.</strong> The highest leverage use of an AI editor in a large repo, for me, has been <em>finding and relating</em> code, not letting it draft whole features on day three. I would rather own fewer lines I understand than ship many I do not.</li>
<li><strong>Context windows are a budget, not a miracle.</strong> Long chats drift. I restart threads when the task changes, and I pin concrete paths or symbols when I know them. Treating the assistant like a stateless search plus narrative layer keeps quality higher than pretending it remembers last week’s decision.</li>
<li><strong>When <code>grep</code> wins:</strong> exact symbol renames, generated migrations, or a single known string across the repo. When Cursor wins: “this concept has five names and three frameworks.”</li>
<li><strong>Telemetry still beats prose.</strong> If logs or traces show which component handled a request, that evidence outranks a confident paragraph from any model. I use AI to suggest <em>where</em> to add a log or breakpoint, not to replace runtime truth.</li>
</ul>
<h3 id="one-technical-detail-request-identity">One technical detail: request identity</h3>
<p>On the REST side, identity often flows through attributes populated by middleware (see the dummy <code>AttachImpersonationContext</code> earlier). That matters because <strong>authorization policies</strong> usually read the same effective user the domain services see. If those two disagree, you get bugs that look like “permissions are random.” When I explore, I explicitly ask how <code>Request</code> attributes, session state, and policy classes align. A boring question, but it prevents spectacular production incidents.</p>
<p>When I want a reproducible check after an exploratory chat, I sometimes leave a scratch assertion in a test or script (nothing that ships), just a guardrail for my own understanding:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">&lt;?</span><span style="color:#a6e22e">php</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Spike / scratch: throw away after you trust the real integration
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">test_create_course_requires_capability</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">actingAsUserWithout</span>(<span style="color:#e6db74">&#39;courses.create&#39;</span>);
</span></span><span style="display:flex;"><span>    $response <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">postJson</span>(<span style="color:#e6db74">&#39;/api/v2/courses&#39;</span>, [<span style="color:#e6db74">&#39;title&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;T&#39;</span>]);
</span></span><span style="display:flex;"><span>    $response<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">assertStatus</span>(<span style="color:#ae81ff">403</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="a-small-playbook-you-can-reuse-tomorrow">A Small Playbook You Can Reuse Tomorrow</h2>
<p>If you take one thing from this post, make it operational:</p>
<ol>
<li><strong>Name the subsystem</strong> (e.g. “impersonation,” “course creation permissions”).</li>
<li><strong>Bound the search</strong> (legacy vs new, UI vs API).</li>
<li><strong>Ask for layers and divergence points</strong>, not just file paths.</li>
<li><strong>Verify</strong> with reads and, when it matters, a debugger.</li>
<li><strong>Log wrong merges</strong> when the model conflates two flows. Those notes train your next prompt.</li>
</ol>
<h2 id="closing">Closing</h2>
<p>The hard part of a large system is rarely typing the implementation. It is knowing which layer owns a rule, whether two stacks agree, and what breaks downstream. Cursor did not hand me that understanding. It narrowed where to look and sharpened the questions I asked in code review and in my own head.</p>
<p>I use it as a <strong>reconnaissance</strong> tool: point, verify, then own the change. That shortened the distance between “new on the team” and “comfortable changing this.” That is the bar I care about for the next task too.</p>
<p>If one belief ties this post together: <strong>in a brownfield codebase, competence shows up as impact awareness, not as commit velocity.</strong> Tools that pull impact into view earlier are worth learning. The rest is packaging.</p>
]]></content:encoded></item></channel></rss>