[{"data":1,"prerenderedAt":675},["ShallowReactive",2],{"profile":3,"projects:page:\u002Fprojects\u002Fclairwire\u002Frelevance":22,"projects:docs:\u002Fprojects\u002Fclairwire":641,"structured-data:current-stack":661},{"id":4,"alias":5,"current":6,"email":10,"extension":11,"headline":12,"links":13,"location":17,"meta":18,"name":19,"stem":20,"__hash__":21},"profile\u002Fprofile.yml","readonly",{"role":7,"company":8,"companyUrl":9},"Fullstack Engineer — Tech Lead","DataCose","https:\u002F\u002Fdatacose.com","redonemini18@gmail.com","yml","Fullstack Engineer",{"github":14,"linkedin":15,"twitch":16},"https:\u002F\u002Fgithub.com\u002FRedoni18","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fredon-emini\u002F","https:\u002F\u002Fwww.twitch.tv\u002Fr3adonly","Pristina, Kosovo",{},"Redon Emini","profile","Fbb1IAp4nuUOOtgXdwpzcqSmG0CUKtZ4D0ifPqdK5QU",{"id":23,"title":24,"body":25,"description":626,"extension":627,"featured":628,"kind":629,"links":630,"meta":631,"navigation":632,"order":634,"path":635,"period":636,"preview":636,"seo":637,"stack":638,"stem":639,"year":636,"__hash__":640},"projects\u002Fprojects\u002Fclairwire\u002F2.relevance.md","Relevance, not keyword hits",{"type":26,"value":27,"toc":620},"minimark",[28,32,35,43,48,51,137,146,374,384,395,399,407,410,414,417,428,431,439,442,446,449,452,470,589,592,603,609,616],[29,30,31],"p",{},"The first version of Clairwire had a simple definition of coverage: if a source was set up to watch a client, everything it returned went into that client's feed. Search X for the client's name, and every result counts.",[29,33,34],{},"That works until real clients show up. A client with a common name gets matched by strangers who share it. A competitor mentioned in passing shows up as if the article were about them. A client who writes for a publication gets every article they've written, because their name is in the byline. None of that is coverage, but all of it was being counted, and \"counted\" means it showed up in the mention total, fed the sentiment chart, and could trigger an alert. The numbers were drifting away from what people were actually saying about the client, and nothing on the screen said so.",[29,36,37,38,42],{},"The mistake underneath is worth naming, because it turns up in lots of systems: ",[39,40,41],"strong",{},"treating how data was fetched as proof of what it is."," A search query decides what gets collected. It can't tell you whether each result is relevant.",[44,45,47],"h2",{"id":46},"five-answers-instead-of-one","Five answers instead of one",[29,49,50],{},"Now, when a mention is analyzed, the model has to make an explicit relevance decision and give its reason. There are five possible answers:",[52,53,54,70],"table",{},[55,56,57],"thead",{},[58,59,60,64,67],"tr",{},[61,62,63],"th",{},"Decision",[61,65,66],{},"What it means",[61,68,69],{},"Counts as coverage?",[71,72,73,88,100,113,125],"tbody",{},[58,74,75,82,85],{},[76,77,78],"td",{},[79,80,81],"code",{},"direct",[76,83,84],{},"The mention itself establishes that it's about this client.",[76,86,87],{},"Yes",[58,89,90,95,98],{},[76,91,92],{},[79,93,94],{},"indirect",[76,96,97],{},"The story is evidently connected to the client, even if the name isn't in the headline.",[76,99,87],{},[58,101,102,107,110],{},[76,103,104],{},[79,105,106],{},"context",[76,108,109],{},"It matched a keyword, competitor or topic the client explicitly asked to follow. Useful background.",[76,111,112],{},"No",[58,114,115,120,123],{},[76,116,117],{},[79,118,119],{},"uncertain",[76,121,122],{},"Not enough evidence either way. Left visible for a person to check.",[76,124,112],{},[58,126,127,132,135],{},[76,128,129],{},[79,130,131],{},"irrelevant",[76,133,134],{},"Unrelated: a namesake, a byline, a coincidental match.",[76,136,112],{},[29,138,139,140,142,143,145],{},"Only ",[79,141,81],{}," and ",[79,144,94],{}," count toward a client's stats, alerts, reports and briefs, and that rule is enforced the same way everywhere. Right after analysis, anything else is stopped before it reaches the alerts queue, and any alert already raised on it is resolved:",[147,148,154],"pre",{"className":149,"code":150,"filename":151,"language":152,"meta":153,"style":153},"language-ts shiki shiki-themes vitesse-light vitesse-dark","\u002F\u002F Context is retained for labeled briefing context, never client alerts.\nif (!['direct', 'indirect'].includes(analysis.relevance ?? 'uncertain')) {\n  await this.prisma.alert.updateMany({\n    where: { organizationId, clientId, mentionId, status: { in: ['new', 'acknowledged'] } },\n    data: { status: 'resolved', resolvedAt: new Date() },\n  })\n  return\n}\n","apps\u002Fapi\u002Fsrc\u002Fmodules\u002Fingestion\u002Fanalysis.processor.ts","ts","",[79,155,156,165,237,265,321,356,362,368],{"__ignoreMap":153},[157,158,161],"span",{"class":159,"line":160},"line",1,[157,162,164],{"class":163},"snYqZ","\u002F\u002F Context is retained for labeled briefing context, never client alerts.\n",[157,166,168,172,176,180,183,187,190,192,195,198,200,202,205,209,212,216,219,222,225,227,229,231,234],{"class":159,"line":167},2,[157,169,171],{"class":170},"sTPum","if",[157,173,175],{"class":174},"si6no"," (",[157,177,179],{"class":178},"s5TCs","!",[157,181,182],{"class":174},"[",[157,184,186],{"class":185},"scnC2","'",[157,188,81],{"class":189},"spP0B",[157,191,186],{"class":185},[157,193,194],{"class":174},",",[157,196,197],{"class":185}," '",[157,199,94],{"class":189},[157,201,186],{"class":185},[157,203,204],{"class":174},"].",[157,206,208],{"class":207},"s_xSY","includes",[157,210,211],{"class":174},"(",[157,213,215],{"class":214},"s9nN2","analysis",[157,217,218],{"class":174},".",[157,220,221],{"class":214},"relevance",[157,223,224],{"class":178}," ??",[157,226,197],{"class":185},[157,228,119],{"class":189},[157,230,186],{"class":185},[157,232,233],{"class":174},"))",[157,235,236],{"class":174}," {\n",[157,238,240,243,247,249,252,254,257,259,262],{"class":159,"line":239},3,[157,241,242],{"class":170},"  await",[157,244,246],{"class":245},"sfsYZ"," this",[157,248,218],{"class":174},[157,250,251],{"class":214},"prisma",[157,253,218],{"class":174},[157,255,256],{"class":214},"alert",[157,258,218],{"class":174},[157,260,261],{"class":207},"updateMany",[157,263,264],{"class":174},"({\n",[157,266,268,272,275,278,281,284,286,289,291,294,296,299,302,304,307,309,311,313,316,318],{"class":159,"line":267},4,[157,269,271],{"class":270},"sHLBJ","    where",[157,273,274],{"class":174},": { ",[157,276,277],{"class":214},"organizationId",[157,279,280],{"class":174},", ",[157,282,283],{"class":214},"clientId",[157,285,280],{"class":174},[157,287,288],{"class":214},"mentionId",[157,290,280],{"class":174},[157,292,293],{"class":270},"status",[157,295,274],{"class":174},[157,297,298],{"class":270},"in",[157,300,301],{"class":174},": [",[157,303,186],{"class":185},[157,305,306],{"class":189},"new",[157,308,186],{"class":185},[157,310,280],{"class":174},[157,312,186],{"class":185},[157,314,315],{"class":189},"acknowledged",[157,317,186],{"class":185},[157,319,320],{"class":174},"] } },\n",[157,322,324,327,329,331,334,336,339,341,343,346,348,350,353],{"class":159,"line":323},5,[157,325,326],{"class":270},"    data",[157,328,274],{"class":174},[157,330,293],{"class":270},[157,332,333],{"class":174},": ",[157,335,186],{"class":185},[157,337,338],{"class":189},"resolved",[157,340,186],{"class":185},[157,342,280],{"class":174},[157,344,345],{"class":270},"resolvedAt",[157,347,333],{"class":174},[157,349,306],{"class":178},[157,351,352],{"class":207}," Date",[157,354,355],{"class":174},"() },\n",[157,357,359],{"class":159,"line":358},6,[157,360,361],{"class":174},"  })\n",[157,363,365],{"class":159,"line":364},7,[157,366,367],{"class":170},"  return\n",[157,369,371],{"class":159,"line":370},8,[157,372,373],{"class":174},"}\n",[29,375,376,377,380,381,383],{},"Look at the fallback in ",[79,378,379],{},"analysis.relevance ?? 'uncertain'",". A missing decision is treated as ",[79,382,119],{},", not as coverage. When the system doesn't know, it doesn't count.",[29,385,386,387,389,390,394],{},"Giving ",[79,388,106],{}," its own category matters more than it might seem. Clients often ",[391,392,393],"em",{},"want"," to follow a competitor or an industry topic. Mixing that into their own numbers would be wrong, but throwing it away would be wrong too. A separate bucket lets it be used where it helps, as background in the daily brief, without inflating anyone's coverage.",[44,396,398],{"id":397},"dont-hide-the-evidence","Don't hide the evidence",[29,400,401,402,142,404,406],{},"It would have been easy to go one step further and only show people the mentions the system is sure about. I decided against it. The mentions inbox still shows ",[79,403,119],{},[79,405,106],{}," items, clearly labeled. The whole point of recording a reason for every decision is that a person can check it, and nobody can check what they can't see.",[29,408,409],{},"When the model gets it wrong, an analyst can mark a mention as \"not about this client\". That one action moves it out of the client's coverage and resolves any alerts it raised, and undo puts everything back the way it was. The model's original decision is never overwritten. The human correction is stored on top of it, so you can always see both what the system thought and what a person decided.",[44,411,413],{"id":412},"writing-a-brief-from-a-day-of-mentions","Writing a brief from a day of mentions",[29,415,416],{},"The daily brief is where relevance pays off most visibly. A busy client can collect hundreds of mentions in a day, and a brief that tries to summarize all of them isn't a brief anymore.",[29,418,419,420,142,422,424,425,427],{},"So the day's ",[79,421,81],{},[79,423,94],{}," mentions are ranked twice: once by reach (how many people are likely to see them) and once by velocity (how fast they're spreading). The top of each ranking is merged into one list, with duplicates removed. A few ",[79,426,106],{}," stories, such as competitor news, are added separately and labeled as context, so they can inform the brief without being presented as the client's own coverage.",[29,429,430],{},"The model that writes the brief only sees that short, pre-selected list, together with the reason each story made the cut. It never sees the raw firehose. The output is more focused that way, and the most expensive and least predictable part of the system, the language model, only ever works on a small input that the rest of the code has already vetted.",[432,433,436],"callout",{"title":434,"type":435},"Selected doesn't mean total","tip",[29,437,438],{},"The stories in a brief are never presented as the day's full volume. A client should never read \"3 stories in your brief\" as \"3 mentions today\".",[29,440,441],{},"The weekly summary that a PR person reviews and then forwards to their own client follows a related rule. The model only gets headlines, source names and numbers, never the full text of the mentions. That text was written by strangers on the internet, and the summary ends up in an email sent outside the organization. Keeping it out of the prompt means nothing a stranger wrote can steer what gets sent.",[44,443,445],{"id":444},"shipping-the-change-without-emptying-anyones-dashboard","Shipping the change without emptying anyone's dashboard",[29,447,448],{},"Changing what counts as coverage is a data migration, even if the database schema barely changes. Apply the stricter rule to existing data, and a client could log in the morning after the deploy to find half their history gone. It would be technically more accurate, it would look exactly like a bug, and it would be a terrible way for anyone to find out the product had changed.",[29,450,451],{},"So the rollout had three parts.",[29,453,454,457,458,461,462,464,465,142,467,469],{},[39,455,456],{},"Old mentions keep their old status."," Every mention analyzed before the change was tagged ",[79,459,460],{},"legacy",", and ",[79,463,460],{}," still counts as coverage alongside ",[79,466,81],{},[79,468,94],{},". The shared filter every coverage query goes through spells that out:",[147,471,474],{"className":149,"code":472,"filename":473,"language":152,"meta":153,"style":153},"\u002F** Keep historical coverage during rollout; new analysis must establish relevance. *\u002F\nexport const clientCoverageWhere = {\n  status: { notIn: ['ignored', 'irrelevant'] },\n  analysis: { is: { relevance: { in: ['direct', 'indirect', 'legacy'] } } },\n} satisfies Prisma.MentionWhereInput\n","apps\u002Fapi\u002Fsrc\u002Fcommon\u002Fcoverage.ts",[79,475,476,481,497,527,572],{"__ignoreMap":153},[157,477,478],{"class":159,"line":160},[157,479,480],{"class":163},"\u002F** Keep historical coverage during rollout; new analysis must establish relevance. *\u002F\n",[157,482,483,486,489,492,495],{"class":159,"line":167},[157,484,485],{"class":170},"export",[157,487,488],{"class":178}," const ",[157,490,491],{"class":214},"clientCoverageWhere",[157,493,494],{"class":174}," =",[157,496,236],{"class":174},[157,498,499,502,504,507,509,511,514,516,518,520,522,524],{"class":159,"line":239},[157,500,501],{"class":270},"  status",[157,503,274],{"class":174},[157,505,506],{"class":270},"notIn",[157,508,301],{"class":174},[157,510,186],{"class":185},[157,512,513],{"class":189},"ignored",[157,515,186],{"class":185},[157,517,280],{"class":174},[157,519,186],{"class":185},[157,521,131],{"class":189},[157,523,186],{"class":185},[157,525,526],{"class":174},"] },\n",[157,528,529,532,534,537,539,541,543,545,547,549,551,553,555,557,559,561,563,565,567,569],{"class":159,"line":267},[157,530,531],{"class":270},"  analysis",[157,533,274],{"class":174},[157,535,536],{"class":270},"is",[157,538,274],{"class":174},[157,540,221],{"class":270},[157,542,274],{"class":174},[157,544,298],{"class":270},[157,546,301],{"class":174},[157,548,186],{"class":185},[157,550,81],{"class":189},[157,552,186],{"class":185},[157,554,280],{"class":174},[157,556,186],{"class":185},[157,558,94],{"class":189},[157,560,186],{"class":185},[157,562,280],{"class":174},[157,564,186],{"class":185},[157,566,460],{"class":189},[157,568,186],{"class":185},[157,570,571],{"class":174},"] } } },\n",[157,573,574,577,580,584,586],{"class":159,"line":323},[157,575,576],{"class":174},"}",[157,578,579],{"class":170}," satisfies",[157,581,583],{"class":582},"s_NWU"," Prisma",[157,585,218],{"class":174},[157,587,588],{"class":582},"MentionWhereInput\n",[29,590,591],{},"Keeping that rule in one exported constant, rather than repeating it in every query, is what made the migration safe. Changing what \"coverage\" means is a one-line edit in one file.",[29,593,594,597,598,600,601,218],{},[39,595,596],{},"New mentions follow the new rules from day one."," Anything analyzed after the deploy has to earn ",[79,599,81],{}," or ",[79,602,94],{},[29,604,605,608],{},[39,606,607],{},"Reclassifying the backlog is a deliberate action."," Old mentions can be re-analyzed under the new rules, but only when someone chooses to, for a specific client and period. Alerts are switched off for that run, so a months-old story doesn't page anyone just because it was scored again. None of it happens automatically just because the code shipped.",[432,610,613],{"title":611,"type":612},"The takeaway","note",[29,614,615],{},"When you change what existing data means, the deploy and the data change don't have to happen at the same moment. Tag what's already there, apply the new rule going forward, and make rewriting history something a person decides to do.",[617,618,619],"style",{},"html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}html pre.shiki code .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}html pre.shiki code .scnC2, html code.shiki .scnC2{--shiki-default:#B5695977;--shiki-dark:#C98A7D77}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .sfsYZ, html code.shiki .sfsYZ{--shiki-default:#A65E2B;--shiki-dark:#C99076}html pre.shiki code .sHLBJ, html code.shiki .sHLBJ{--shiki-default:#998418;--shiki-dark:#B8A965}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s_NWU, html code.shiki .s_NWU{--shiki-default:#2E8F82;--shiki-dark:#5DA994}",{"title":153,"searchDepth":167,"depth":239,"links":621},[622,623,624,625],{"id":46,"depth":167,"text":47},{"id":397,"depth":167,"text":398},{"id":412,"depth":167,"text":413},{"id":444,"depth":167,"text":445},"Why a mention containing a client's name isn't the same as a mention about them, how Clairwire records the difference, and how the change shipped without emptying anyone's dashboard.","md",false,"personal",{},{},{"title":633},"Relevance",999,"\u002Fprojects\u002Fclairwire\u002Frelevance",null,{"title":24,"description":626},[],"projects\u002Fclairwire\u002F2.relevance","cFvnuUxRHlsl-wZpNZUtOLZIRPhSmLNtz_Y6GX6XcxA",[642,652,659],{"path":643,"title":644,"stem":645,"navigation":646,"description":647,"preview":648},"\u002Fprojects\u002Fclairwire","Clairwire","projects\u002Fclairwire\u002Findex",true,"Media monitoring for PR teams. Everything said about your clients in one place, and an alert when it matters.",{"src":649,"alt":650,"og":651},"\u002Fprojects\u002Fclairwire\u002Fpreview.webp","Clairwire: 'Every mention. Understood.' beside an analysis card showing sentiment, credibility and virality scores with a plain-language reason","\u002Fprojects\u002Fclairwire\u002Fhero.png",{"path":653,"title":654,"stem":655,"navigation":656,"description":658,"preview":636},"\u002Fprojects\u002Fclairwire\u002Farchitecture","One contract, many sources","projects\u002Fclairwire\u002F1.architecture",{"title":657},"Architecture","How Clairwire stops a dozen different platforms from becoming a dozen special cases, with one normalized shape, adapters, capability flags, job queues and an AI layer behind an interface.",{"path":635,"title":24,"stem":639,"navigation":660,"description":626,"preview":636},{"title":633},[662,663,664,665,666,667,668,669,670,671,672,673,674],"TypeScript","Vue 3","Nuxt","Pinia","Tailwind CSS","Python","FastAPI","SQLAlchemy","Pydantic","PostgreSQL","Supabase","GCP","Firebase",1790471331919]