Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ jobs:
- run: cd workers/agent-runtime && npm ci --include=dev
- run: npm run verify:moltbot-tools
- run: npm run verify:litellm-patch-runner
- run: npm run verify:readme-invariants
# These node:test files lived only in the retired all-in-one job.
- name: Run agent service tests
run: |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
Bring your workflow and your agents.<br/>
Your team works with them in one pod.

**[Try the cloud agent →](https://commonly.me)** &nbsp;&nbsp;&nbsp; **[Bring your own →](#bring-your-own)**
**[Try the cloud agent →](https://commonly.me)** &nbsp;&nbsp;&nbsp; **[Bring your own →](#bring-your-own)** &nbsp;&nbsp;&nbsp; **[Discord →](https://discord.gg/NsS3fzsJDw)**

[![Discord](https://img.shields.io/badge/Discord-join%20the%20community-5865F2?logo=discord&logoColor=white)](https://discord.gg/NsS3fzsJDw) &nbsp; [Join the community](https://discord.gg/NsS3fzsJDw)

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"verify:litellm-patch-runner": "node scripts/verify-litellm-patch-runner.js",
"verify:numbered-rules": "node scripts/verify-numbered-rules.js",
"verify:ax-audit-numbering": "node scripts/verify-ax-audit-numbering.js",
"verify:readme-invariants": "node scripts/verify-readme-invariants.js && node scripts/verify-readme-invariants.js --self-test",
"prepare": "husky"
},
"devDependencies": {
Expand Down
146 changes: 146 additions & 0 deletions scripts/verify-readme-invariants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/usr/bin/env node
/**
* README invariant check.
*
* The official Discord invite went into the README with a top badge on 07-19
* (#694, #695). The 10-23 rewrite (#2477) dropped the badge, so Discord
* survived only as a footer link, and nothing noticed until Sam did. This check
* is the thing that notices: it fails, naming each missing item, when the
* README stops carrying what a visitor needs.
*
* The rules are below. To change them, edit RULES here and record the reason in
* the README benchmark brief (TASK-422), which is where the README's
* structure is argued. Do not add a rule without a reason there.
*
* Usage: node scripts/verify-readme-invariants.js [path]
* node scripts/verify-readme-invariants.js --self-test
*/

const fs = require('fs');
const path = require('path');

const DEFAULT_FILE = path.join(__dirname, '..', 'README.md');
const TOP_LINES = 40;
const DISCORD_INVITE = 'https://discord.gg/NsS3fzsJDw';
// The bare host/path, for messages and self-test labels. Never used to match a URL.
const DISCORD_LABEL = 'discord.gg/NsS3fzsJDw';

// Compare parsed URLs, not substrings: `includes(url)` would accept
// `https://evil.example/?x=https://discord.gg/NsS3fzsJDw`.
function hasDiscordInvite(lines) {
const urls = lines.join('\n').match(/https?:\/\/[^\s)"'<>]+/g) || [];
const invite = new URL(DISCORD_INVITE);
return urls.some((raw) => {
try {
const url = new URL(raw);
return url.protocol === invite.protocol
&& url.hostname === invite.hostname
&& url.pathname === invite.pathname;
} catch (err) {
return false;
}
});
}

// Link targets are parsed, not matched as prefixes: a prefix test accepts
// `https://docs.commonly.me@evil.example/` and `https://docs.commonly.me.evil.example/`.
function linkTargets(lines) {
return [...lines.join('\n').matchAll(/\]\(([^)\s]+)/g)].map((m) => m[1]);
}

function isDocsTarget(target) {
if (target.startsWith('docs/')) return true;
try {
const url = new URL(target);
return url.protocol === 'https:' && url.hostname === 'docs.commonly.me';
} catch (err) {
return false;
}
}

// Each rule: a name a reader can act on, and a test over the README text.
const RULES = [
{
name: `the Discord invite (${DISCORD_LABEL}) within the first ${TOP_LINES} lines`,
check: (lines) => hasDiscordInvite(lines.slice(0, TOP_LINES)),
},
{
name: 'a link to the docs',
check: (lines) => linkTargets(lines).some(isDocsTarget),
},
{
name: 'a link to the license (LICENSE)',
check: (lines) => /\]\((\.\/)?LICENSE\)/.test(lines.join('\n')),
},
{
name: 'a quickstart section or link',
check: (lines) => /^#{1,6} .*quick ?start/im.test(lines.join('\n'))
|| /\]\([^)]*quickstart[^)]*\)/i.test(lines.join('\n')),
},
];

function checkReadme(text) {
const lines = text.split('\n');
return RULES.filter((rule) => !rule.check(lines)).map((rule) => `missing ${rule.name}`);
}

function selfTest() {
const complete = [
`[![Discord](https://img.shields.io/badge/chat-discord-blue)](${DISCORD_INVITE})`,
'## Quick Start',
'[Docs](docs/README.md) · [License](LICENSE)',
].join('\n');
if (checkReadme(complete).length !== 0) {
throw new Error('self-test: a complete fixture was reported as missing something');
}

// A README without the invite must fail, and must name the invite.
const noInvite = complete.replace(DISCORD_INVITE, 'https://example.com');
const problems = checkReadme(noInvite);
if (!problems.some((p) => p.includes(DISCORD_LABEL))) {
throw new Error(`self-test: a README without the invite did not fail naming it: ${JSON.stringify(problems)}`);
}

// A docs link must resolve to the docs host, not merely start with it.
for (const bogus of ['https://docs.commonly.me@evil.example/', 'https://docs.commonly.me.evil.example/']) {
const fake = complete.replace('[Docs](docs/README.md)', `[Docs](${bogus})`);
if (!checkReadme(fake).some((p) => p.includes('a link to the docs'))) {
throw new Error(`self-test: a docs link to ${bogus} was accepted`);
}
}

// The invite carried in another host's query string is not the invite.
const lookalike = complete.replace(DISCORD_INVITE, `https://example.com/?u=${DISCORD_INVITE}`);
if (!checkReadme(lookalike).some((p) => p.includes(DISCORD_LABEL))) {
throw new Error('self-test: a lookalike URL carrying the invite was accepted');
}

// The invite past line 40 is a footer, not a top link: it must fail too.
const padded = `${'filler\n'.repeat(TOP_LINES)}${DISCORD_INVITE}\n## Quick Start\n[D](https://docs.commonly.me/quickstart)\n[L](LICENSE)`;
if (!checkReadme(padded).some((p) => p.includes(`within the first ${TOP_LINES} lines`))) {
throw new Error('self-test: an invite below the top lines was accepted');
}
console.log('readme invariants self-test: ok');
}

function main(argv) {
if (argv.includes('--self-test')) {
selfTest();
return 0;
}
const file = argv.find((a) => !a.startsWith('--')) || DEFAULT_FILE;
const problems = checkReadme(fs.readFileSync(file, 'utf8'));
if (problems.length > 0) {
console.error(`README invariants failed for ${path.relative(process.cwd(), file)}:`);
for (const p of problems) console.error(` - ${p}`);
return 1;
}
console.log(`README invariants: ok (${RULES.length} rules)`);
return 0;
}

if (require.main === module) {
process.exitCode = main(process.argv.slice(2));
}

module.exports = { checkReadme, RULES, DISCORD_INVITE };
Loading