Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fa3d21f
Rebuild recruitment management for four forums
SimplyJpk Sep 29, 2026
1637cef
Trim recruitment Guidelines to 1,000 characters
SimplyJpk Sep 29, 2026
fb9a9da
style(editorconfig): reorganize settings for consistency and clarity
SimplyJpk Oct 14, 2026
988fe7d
fix(launch): update program path to .NET 8 for DiscordBot
SimplyJpk Oct 14, 2026
c42b996
fix(vscode): update launch and task configurations for improved debug…
SimplyJpk Oct 14, 2026
e6f2543
Merge branch 'master' into feat/recruitment-babysitter
SimplyJpk Oct 14, 2026
78be5d6
Fix staged recruitment forum setup
SimplyJpk Oct 14, 2026
40b67f4
Drive recruitment from events and scheduled deadlines
SimplyJpk Oct 14, 2026
f2bbef2
Document recruitment recovery and feed status
SimplyJpk Oct 14, 2026
63cde11
Fix recruitment staff exclusion and deletion recovery
SimplyJpk Oct 14, 2026
70732cf
Present recruitment checks in purpose-specific embeds
SimplyJpk Oct 14, 2026
02856a6
Manage pinned recruitment references and recovery
SimplyJpk Oct 14, 2026
b8ac1d6
Preserve recruitment history and honor timely acknowledgements
SimplyJpk Oct 15, 2026
b906fb0
Limit repeated recruitment attempts and expose eligibility
SimplyJpk Oct 15, 2026
b027221
Add neutral reader notes and private recruitment reports
SimplyJpk Oct 15, 2026
d00f5a3
Count recruitment attempts deleted before the first read
SimplyJpk Oct 15, 2026
0ed9016
Clarify recruitment eligibility and guidance
SimplyJpk Oct 17, 2026
9edb1df
Add recruitment observations and author identity
SimplyJpk Oct 17, 2026
8a90fac
Manage portfolio sites and preserve private URL identities
SimplyJpk Oct 17, 2026
1bfc4b2
Ignore zero-valued compensation claims
SimplyJpk Oct 17, 2026
8f24992
Render recruitment observation cards and legends
SimplyJpk Oct 17, 2026
ea75fdf
Deliver recoverable recruitment cards with retained media
SimplyJpk Oct 17, 2026
9a54f42
Pin recruitment responses with durable recovery
SimplyJpk Oct 17, 2026
98cd515
fix: A number of tweaks to recruitment service and rendering
SimplyJpk Oct 19, 2026
8e48438
Organize recruitment responsibilities and test suites
SimplyJpk Oct 19, 2026
96e5004
Separate recruitment validation and policy decisions
SimplyJpk Oct 19, 2026
32a32e3
Reuse listing analysis and isolate public rendering
SimplyJpk Oct 19, 2026
473beaf
Share message recovery and scope Discord compatibility requests
SimplyJpk Oct 19, 2026
7e797b9
Extract listing lifecycle and durable action execution
SimplyJpk Oct 19, 2026
cc865c9
Separate recruitment commands and query formatting
SimplyJpk Oct 19, 2026
b0b2862
Separate forum setup and inventory recovery
SimplyJpk Oct 19, 2026
14554eb
Separate recruitment runtime, scheduling and health
SimplyJpk Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
root = true

[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
indent_style = space
indent_size = 4

[*.cs]
# Unused private members
dotnet_diagnostic.IDE0051.severity = warning
dotnet_diagnostic.IDE0052.severity = warning
tab_width = 4

# Discord.Net discovers command handlers via reflection — they appear unused to static analysis
[DiscordBot/Modules/**.cs]
dotnet_diagnostic.IDE0051.severity = none

[*.json]
indent_size = 2
52 changes: 26 additions & 26 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/DiscordBot/bin/Debug/netcoreapp2.1/DiscordBot.dll",
"args": [],
"cwd": "${workspaceFolder}/DiscordBot",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
]
}
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// The build task writes here regardless of the project's target framework.
"program": "${workspaceFolder}/DiscordBot/bin/vscode/DiscordBot.dll",
"args": [],
"cwd": "${workspaceFolder}/DiscordBot",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
]
}
102 changes: 61 additions & 41 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -1,42 +1,62 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/DiscordBot/DiscordBot.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/DiscordBot/DiscordBot.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"${workspaceFolder}/DiscordBot/DiscordBot.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
}
]
}
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/DiscordBot/DiscordBot.csproj",
"--configuration",
"Debug",
"--output",
"${workspaceFolder}/DiscordBot/bin/vscode",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/DiscordBot/DiscordBot.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"--project",
"${workspaceFolder}/DiscordBot/DiscordBot.csproj",
"run"
],
"options": {
"cwd": "${workspaceFolder}/DiscordBot"
},
"problemMatcher": "$msCompile"
},
{
"label": "run",
"command": "dotnet",
"type": "process",
"args": [
"run",
"--project",
"${workspaceFolder}/DiscordBot/DiscordBot.csproj"
],
"options": {
"cwd": "${workspaceFolder}/DiscordBot"
},
"problemMatcher": "$msCompile"
}
]
}
1 change: 1 addition & 0 deletions DiscordBot.Tests/DiscordBot.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RollForward>Major</RollForward>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
Expand Down
36 changes: 36 additions & 0 deletions DiscordBot.Tests/Recruitment/Analysis/ListingAnalysisTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using DiscordBot.Services.Recruitment;
using DiscordBot.Services.Recruitment.Domain.Analysis;
using DiscordBot.Utils;

namespace DiscordBot.Tests.Recruitment;

public class ListingAnalysisTests
{
[Theory]
[InlineData("github.com", true)]
[InlineData("USER.GitHub.Com", true)]
[InlineData("fakegithub.com", false)]
[InlineData("github.com.example.org", false)]
public void DomainMatchingRequiresRootOrDotBoundary(string host, bool expected) =>
Assert.Equal(expected, DomainName.Matches(host, "github.com"));

[Fact]
public async Task EmptyRegistryChangesChecksButPreservesSeedBasedSummaryAndEvidence()
{
using var rig = new RecruitmentRig(); await rig.Start();
var post = await rig.Post();
var live = rig.Discord.Threads[post.Id] with { Starter = "https://github.com/Example/CaseSensitive?utm_source=chat" };
var analysis = ListingAnalysis.Read(live, "paid-for-hire", new HashSet<string>());
Assert.False(Assert.Single(analysis.Portfolio.Candidates).Supported);
Assert.Equal("work-sample link detected", analysis.PortfolioSummary);
Assert.Equal("https://github.com/Example/CaseSensitive", Assert.Single(analysis.ContactLinks));
Assert.Equal(RecruitmentContent.Observations(live.Starter), analysis.Observations);
}

[Fact]
public void LiteralDisplayTruncatesBeforeReplacingControlAndFormattingCharacters()
{
Assert.Equal("A ", DiscordDisplayText.Literal("A\n@*Z", 4));
Assert.Equal("café 🦆", DiscordDisplayText.Literal("café 🦆"));
}
}
71 changes: 71 additions & 0 deletions DiscordBot.Tests/Recruitment/Analysis/RecruitmentContentTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using DiscordBot.Services.Recruitment;

namespace DiscordBot.Tests.Recruitment;

public class RecruitmentContentTests
{
[Theory]
[InlineData("Unpaid volunteer project. The game will sell for $20. DM for details.", "unpaid collaboration stated")]
[InlineData("The game sells for $20", "amount mentioned; compensation unclear")]
[InlineData("Application fee $20 to join", "amount mentioned; compensation unclear")]
[InlineData("Budget: AUD 500 for the project", "concrete rate or budget stated")]
[InlineData("My rate is $40/hour", "concrete rate or budget stated")]
[InlineData("Unpaid role. Budget $500 for some tasks", "mixed compensation terms; clarify what is guaranteed")]
[InlineData("Revenue share, plus a rate of $30/hour", "mixed compensation terms; clarify what is guaranteed")]
[InlineData("Revenue share only", "revenue-share stated")]
public void PaymentNeedsCompensationContextAndPreservesConflicts(string content, string summary)
{
var observation = RecruitmentContent.ReadPayment(content);
Assert.Equal(summary, observation.Summary); Assert.NotEmpty(observation.Evidence);
}

[Theory]
[InlineData("No portfolio available; apply at https://example.org/contact", "no work-sample link found")]
[InlineData("Portfolio coming soon: https://example.org/contact", "no work-sample link found")]
[InlineData("Not my work: https://artstation.com/artist", "external link found; purpose unclear")]
[InlineData("My portfolio: https://example.org/work", "work-sample link detected")]
public void NegatedWorkClaimsDoNotClearSampleCheck(string content, string expected) => Assert.Equal(expected, RecruitmentContent.Portfolio(content));

[Theory]
[InlineData("image/png", "media sample attached")]
[InlineData("video/mp4", "media sample attached")]
[InlineData("audio/ogg", "media sample attached")]
[InlineData("application/octet-stream", "no work-sample link found")]
public void MediaAttachmentsCanSupplySamplesWithoutTreatingExecutablesAsProof(string type, string expected) =>
Assert.Equal(expected, RecruitmentContent.Portfolio("Samples attached", new[] { new StarterAttachment("1", "sample", type) }));

[Theory]
[InlineData("DM for rates and payment terms", "private-terms")]
[InlineData("Payment details only in DMs", "private-terms")]
[InlineData("Pay an application fee of $10", "applicant-fee")]
[InlineData("No refunds for the application fee", "applicant-fee")]
[InlineData("Send money immediately to reserve the job", "payment-pressure")]
public void BehavioralObservationsRetainAnExcerpt(string content, string code)
{
var observation = Assert.Single(RecruitmentContent.Observations(content), o => o.Code == code);
Assert.Contains(content, observation.Evidence);
}

[Theory]

[InlineData("No application fees or deposits required.")]
[InlineData("Do not DM for payment details; terms must stay public.")]
[InlineData("Never send money immediately to someone you do not know.")]
public void OrdinaryContactAndExplicitSafetyAdviceAreNotPressure(string content) => Assert.Empty(RecruitmentContent.Observations(content));

[Fact]
public void DmToApplyIsPublicFirstClarificationWithoutInventingPaymentPressure()
{
const string text = "Budget USD 500. Deliver in two weeks. DM to apply with your portfolio.";
Assert.Empty(RecruitmentContent.Observations(text));
Assert.NotEmpty(RecruitmentContent.DmRequest(text)!);
}

[Fact]
public void HostRootsAndOverlongContentDoNotCreateBroadContactMatches()
{
Assert.Empty(RecruitmentContent.ContactLinks("https://github.com https://example.org/"));
Assert.Equal(new[] { "https://example.org/my-project" }, RecruitmentContent.ContactLinks("https://example.org/my-project?utm_source=one https://example.org/my-project?utm_source=two"));
Assert.Empty(RecruitmentContent.Observations(new string('a', 8000) + " DM for rates"));
}
}
Loading
Loading