Skip to content

Commit 9710ade

Browse files
authored
Merge pull request #52 from Forward-Future/codex/newest-loop-order
[codex] Order loops and add contributor playbook
2 parents 5c1c2c0 + a82d42f commit 9710ade

106 files changed

Lines changed: 794 additions & 430 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

scripts/audit-seo-geo.mjs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -245,20 +245,24 @@ for (const [relativePath, html] of pages) {
245245
);
246246
}
247247

248-
if (loop.sourceUrl) {
249-
if (!html.includes(`href="${loop.sourceUrl}"`) || !html.includes("isBasedOn")) {
250-
addFinding(
251-
"high",
252-
"source citations",
253-
"Contributed loop is missing its visible source link or isBasedOn schema.",
254-
relativePath,
255-
);
256-
}
257-
} else if (!pageText.includes(`Contributed by ${loop.author}`)) {
248+
if (!pageText.includes(`Contributed by ${loop.author}`)) {
258249
addFinding(
259250
"medium",
260-
"source citations",
261-
"Original loop is missing visible contributor attribution.",
251+
"contributor attribution",
252+
"Published loop is missing visible contributor attribution.",
253+
relativePath,
254+
);
255+
}
256+
257+
if (
258+
html.includes('class="detail-source-link"') ||
259+
html.includes("isBasedOn") ||
260+
(loop.sourceUrl && html.includes(loop.sourceUrl))
261+
) {
262+
addFinding(
263+
"high",
264+
"source privacy",
265+
"Published loop exposes a source link.",
262266
relativePath,
263267
);
264268
}

scripts/build-loop-pages.mjs

Lines changed: 99 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { mkdir, rm, writeFile } from "node:fs/promises";
1+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
22
import { fileURLToPath } from "node:url";
33
import path from "node:path";
44

55
import { escapeJsonForHtmlScript } from "./html-script-utils.mjs";
6-
import { loops, site } from "./loop-data.mjs";
6+
import { getLoopCategory, loops, site } from "./loop-data.mjs";
77

88
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
99
const outputRoot = path.join(root, "site");
@@ -39,6 +39,46 @@ function socialImageUrl(loop) {
3939
return `${site.baseUrl}assets/social/${loop.slug}-${site.socialImageVersion}.${site.socialImageExtension}`;
4040
}
4141

42+
async function syncHomepagePublicationDates() {
43+
const homepagePath = path.join(outputRoot, "index.html");
44+
let homepage = await readFile(homepagePath, "utf8");
45+
46+
for (const loop of loops) {
47+
const loopHref = `href="./loops/${loop.slug}/"`;
48+
const hrefIndex = homepage.indexOf(loopHref);
49+
const rowStart = homepage.lastIndexOf("<tr", hrefIndex);
50+
const rowTagEnd = homepage.indexOf(">", rowStart);
51+
52+
if (hrefIndex < 0 || rowStart < 0 || rowTagEnd < 0) {
53+
throw new Error(`Could not find the homepage row for ${loop.slug}.`);
54+
}
55+
56+
const rowTag = homepage.slice(rowStart, rowTagEnd + 1);
57+
const categoryAttribute = `data-category="${getLoopCategory(loop).slug}"`;
58+
const publishedAttribute = `data-published="${loop.published}"`;
59+
const normalizedRowTag = rowTag.replace(
60+
/\n\s*data-published="[^"]*"/,
61+
"",
62+
);
63+
64+
if (!normalizedRowTag.includes(categoryAttribute)) {
65+
throw new Error(`Homepage category drift for ${loop.slug}.`);
66+
}
67+
68+
const updatedRowTag = normalizedRowTag.replace(
69+
categoryAttribute,
70+
`${categoryAttribute}\n ${publishedAttribute}`,
71+
);
72+
73+
homepage =
74+
homepage.slice(0, rowStart) +
75+
updatedRowTag +
76+
homepage.slice(rowTagEnd + 1);
77+
}
78+
79+
await writeFile(homepagePath, homepage);
80+
}
81+
4282
function shareActions(loop, url) {
4383
const postText = `Try "${loop.title}" from the Loop Library: ${loop.summary}`;
4484

@@ -72,6 +112,58 @@ function relatedLinks(loop) {
72112
.join("");
73113
}
74114

115+
function playbookList(items) {
116+
return items
117+
.map((item) => ` <li>${escapeHtml(item)}</li>`)
118+
.join("\n");
119+
}
120+
121+
function contributorPlaybook(loop) {
122+
const playbook = loop.contributorPlaybook;
123+
124+
if (!playbook) {
125+
return "";
126+
}
127+
128+
return `
129+
<details class="detail-more contributor-playbook">
130+
<summary>
131+
<span>Contributor playbook</span>
132+
<small>Boundaries, required outputs, implementation guidance, and reviewer handoff</small>
133+
</summary>
134+
135+
<div class="detail-more-body contributor-playbook-body">
136+
<section aria-labelledby="contributor-when-not-to-use">
137+
<h2 id="contributor-when-not-to-use">Do not use this when</h2>
138+
<ul class="contributor-playbook-list">
139+
${playbookList(playbook.whenNotToUse)}
140+
</ul>
141+
</section>
142+
143+
<section aria-labelledby="contributor-expected-outputs">
144+
<h2 id="contributor-expected-outputs">Required outputs</h2>
145+
<ul class="contributor-playbook-list contributor-playbook-list--grid">
146+
${playbookList(playbook.expectedOutputs)}
147+
</ul>
148+
</section>
149+
150+
<section aria-labelledby="contributor-implementation-guidance">
151+
<h2 id="contributor-implementation-guidance">Match the method to the artifact</h2>
152+
<ul class="contributor-playbook-list">
153+
${playbookList(playbook.implementationGuidance)}
154+
</ul>
155+
</section>
156+
157+
<section aria-labelledby="contributor-reviewer-handoff">
158+
<h2 id="contributor-reviewer-handoff">Reviewer handoff</h2>
159+
<ul class="contributor-playbook-list">
160+
${playbookList(playbook.reviewerHandoff)}
161+
</ul>
162+
</section>
163+
</div>
164+
</details>`;
165+
}
166+
75167
function hereNowCredit(assetPath, modifier) {
76168
return `<a
77169
class="here-now-credit here-now-credit--${modifier}"
@@ -142,7 +234,6 @@ function structuredData(loop) {
142234
dateModified: loop.modified,
143235
articleSection: loop.categoryLabel,
144236
keywords: loop.keywords,
145-
...(loop.sourceUrl ? { isBasedOn: loop.sourceUrl } : {}),
146237
image: {
147238
"@type": "ImageObject",
148239
url: imageUrl,
@@ -248,11 +339,11 @@ function renderLoopPage(loop) {
248339
<link rel="alternate" type="text/plain" title="${escapeHtml(site.name)} plain-text catalog" href="${escapeHtml(site.baseUrl)}catalog.txt" />
249340
<link rel="help" href="${escapeHtml(site.baseUrl)}agents/" />
250341
<link rel="icon" type="image/png" href="../../assets/favicon.png" />
251-
<link rel="stylesheet" href="../../styles.css?v=20260620-primary-nav" />
342+
<link rel="stylesheet" href="../../styles.css?v=20260620-newest-first" />
252343
<script type="application/ld+json">
253344
${structuredData(loop)}
254345
</script>
255-
<script src="../../script.js?v=20260620-primary-nav" defer></script>
346+
<script src="../../script.js?v=20260620-newest-first" defer></script>
256347
<title>${escapeHtml(loop.seoTitle)}</title>
257348
</head>
258349
<body>
@@ -313,11 +404,7 @@ ${structuredData(loop)}
313404
<h1>${escapeHtml(loop.title)}</h1>
314405
<p class="detail-lede">${escapeHtml(loop.description)}</p>
315406
<p class="detail-byline">
316-
Contributed by <strong>${escapeHtml(loop.author)}</strong>${
317-
loop.sourceUrl
318-
? ` · <a class="detail-source-link" href="${escapeHtml(loop.sourceUrl)}" target="_blank" rel="noopener noreferrer">Source</a>`
319-
: ""
320-
}
407+
Contributed by <strong>${escapeHtml(loop.author)}</strong>
321408
</p>
322409
${shareActions(loop, url)}
323410
</header>
@@ -392,6 +479,7 @@ ${relatedLinks(loop)}
392479
</nav>
393480
</div>
394481
</details>
482+
${contributorPlaybook(loop)}
395483
</div>
396484
</article>
397485
</main>
@@ -484,6 +572,7 @@ ${loops
484572
`;
485573
}
486574

575+
await syncHomepagePublicationDates();
487576
await rm(path.join(outputRoot, "loops"), { recursive: true, force: true });
488577

489578
for (const loop of loops) {

scripts/build-skill-catalog.mjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ export function renderCatalogJson() {
113113
steps: loop.steps,
114114
why: loop.why,
115115
implementationNote: loop.note,
116+
...(loop.contributorPlaybook
117+
? { contributorPlaybook: loop.contributorPlaybook }
118+
: {}),
116119
keywords: loop.keywords,
117120
related: loop.related.map((slug) => {
118121
const relatedLoop = loopBySlug.get(slug);
@@ -123,7 +126,6 @@ export function renderCatalogJson() {
123126
url: `${site.baseUrl}loops/${slug}/`,
124127
};
125128
}),
126-
...(loop.sourceUrl ? { sourceUrl: loop.sourceUrl } : {}),
127129
};
128130
}),
129131
};

scripts/check.mjs

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,10 @@ for (const [index, loop] of loops.entries()) {
475475
assert.deepEqual(catalogLoop.steps, loop.steps);
476476
assert.equal(catalogLoop.why, loop.why);
477477
assert.equal(catalogLoop.implementationNote, loop.note);
478+
assert.deepEqual(
479+
catalogLoop.contributorPlaybook,
480+
loop.contributorPlaybook,
481+
);
478482
assert.deepEqual(catalogLoop.keywords, loop.keywords);
479483
assert.deepEqual(
480484
catalogLoop.related.map(({ slug }) => slug),
@@ -490,7 +494,7 @@ for (const [index, loop] of loops.entries()) {
490494
(relatedSlug) => `${siteMeta.baseUrl}loops/${relatedSlug}/`,
491495
),
492496
);
493-
assert.equal(catalogLoop.sourceUrl, loop.sourceUrl);
497+
assert.equal(catalogLoop.sourceUrl, undefined);
494498
assert(loop.related.every((relatedSlug) => slugs.has(relatedSlug)));
495499
assert(html.includes(loop.title));
496500
assert(normalizedHomepageRow.includes(loop.prompt));
@@ -501,6 +505,7 @@ for (const [index, loop] of loops.entries()) {
501505
);
502506
assert(!homepageRow.includes('class="cell-number"'));
503507
assert(homepageRow.includes(`data-category="${category.slug}"`));
508+
assert(homepageRow.includes(`data-published="${loop.published}"`));
504509
assert(
505510
homepageRow.includes(
506511
`<span class="loop-category">${category.label}</span>`,
@@ -551,8 +556,8 @@ for (const [index, loop] of loops.entries()) {
551556
),
552557
);
553558
assert(page.includes(`rel="help" href="${siteMeta.baseUrl}agents/"`));
554-
assert(page.includes("../../styles.css?v=20260620-primary-nav"));
555-
assert(page.includes("../../script.js?v=20260620-primary-nav"));
559+
assert(page.includes("../../styles.css?v=20260620-newest-first"));
560+
assert(page.includes("../../script.js?v=20260620-newest-first"));
556561
assert(page.includes(`<meta property="og:image" content="${imageUrl}"`));
557562
assert(page.includes(`<meta property="og:image:secure_url" content="${imageUrl}"`));
558563
assert(page.includes(`<meta property="og:image:type" content="${siteMeta.socialImageMimeType}"`));
@@ -616,6 +621,22 @@ for (const [index, loop] of loops.entries()) {
616621
assert(page.includes("How to run it"));
617622
assert(page.includes("Why it works"));
618623
assert(page.includes("Implementation note"));
624+
if (loop.contributorPlaybook) {
625+
assert(page.includes('class="detail-more contributor-playbook"'));
626+
assert(page.includes("Contributor playbook"));
627+
assert(page.includes("Do not use this when"));
628+
assert(page.includes("Required outputs"));
629+
assert(page.includes("Match the method to the artifact"));
630+
assert(page.includes("Reviewer handoff"));
631+
assert(
632+
Object.values(loop.contributorPlaybook)
633+
.flat()
634+
.every((item) => page.includes(escapeHtml(item))),
635+
);
636+
} else {
637+
assert.equal(catalogLoop.contributorPlaybook, undefined);
638+
assert(!page.includes('class="detail-more contributor-playbook"'));
639+
}
619640
assert(!page.includes("<h2>Topics</h2>"));
620641
assert(page.includes("Related loops"));
621642
assert(!page.includes("<dt>Type</dt>"));
@@ -695,17 +716,9 @@ for (const [index, loop] of loops.entries()) {
695716
escapeHtml(loopBySlug.get(relatedSlug).title),
696717
),
697718
);
698-
if (loop.sourceUrl) {
699-
assert.equal(article.isBasedOn, loop.sourceUrl);
700-
assert(
701-
page.includes(
702-
`<a class="detail-source-link" href="${escapeHtml(loop.sourceUrl)}" target="_blank" rel="noopener noreferrer">Source</a>`,
703-
),
704-
);
705-
} else {
706-
assert.equal(article.isBasedOn, undefined);
707-
assert(!page.includes('class="detail-source-link"'));
708-
}
719+
assert.equal(article.isBasedOn, undefined);
720+
assert(!page.includes('class="detail-source-link"'));
721+
assert(!loop.sourceUrl || !page.includes(loop.sourceUrl));
709722
assert(sitemap.includes(`<loc>${url}</loc>`));
710723
assert(sitemap.includes(`<lastmod>${loop.modified}</lastmod>`));
711724
assert(feed.includes(`<id>${url}</id>`));
@@ -815,8 +828,10 @@ assert(!html.includes('data-type='));
815828
assert(!html.includes('class="cell-type"'));
816829
assert(!html.includes("type-badge"));
817830
assert(!html.includes('<th scope="col">Type</th>'));
818-
assert(html.includes("./styles.css?v=20260620-primary-nav"));
819-
assert(html.includes("./script.js?v=20260620-primary-nav"));
831+
assert(html.includes("./styles.css?v=20260620-newest-first"));
832+
assert(html.includes("./script.js?v=20260620-newest-first"));
833+
assert(script.includes("const publishedDifference = b.dataset.published.localeCompare("));
834+
assert(script.includes("return loopRowPositions.get(b) - loopRowPositions.get(a);"));
820835
const homepagePostText =
821836
"Find Loops and create your own - Loop Library";
822837
assert(html.includes('class="share-actions" aria-label="Share Loop Library"'));
@@ -877,8 +892,8 @@ assert.equal(
877892
(learnHtml.match(/href="https:\/\/here\.now\/r\/signals"/g) || []).length,
878893
2,
879894
);
880-
assert(learnHtml.includes("../styles.css?v=20260620-article-layout"));
881-
assert(learnHtml.includes("../script.js?v=20260620-primary-nav"));
895+
assert(learnHtml.includes("../styles.css?v=20260620-newest-first"));
896+
assert(learnHtml.includes("../script.js?v=20260620-newest-first"));
882897
assert(learnHtml.includes("How agent loops work"));
883898
assert(learnHtml.includes('<meta name="robots" content="index, follow"'));
884899
assert(learnHtml.includes("What makes a loop useful"));
@@ -926,8 +941,8 @@ assert(agentHtml.includes("npx skills add Forward-Future/loop-library --skill lo
926941
assert(agentHtml.includes('<meta name="robots" content="index, follow"'));
927942
assert(agentHtml.includes(`href="${siteMeta.baseUrl}catalog.json"`));
928943
assert(agentHtml.includes(`href="${siteMeta.baseUrl}llms.txt"`));
929-
assert(agentHtml.includes("../styles.css?v=20260620-article-layout"));
930-
assert(agentHtml.includes("../script.js?v=20260620-primary-nav"));
944+
assert(agentHtml.includes("../styles.css?v=20260620-newest-first"));
945+
assert(agentHtml.includes("../script.js?v=20260620-newest-first"));
931946
assert(html.includes("Repeatable AI Agent Workflows"));
932947
assert(html.includes('rel="sitemap"'));
933948
assert(html.includes(`href="${siteMeta.baseUrl}catalog.json"`));

0 commit comments

Comments
 (0)