mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-26 22:51:47 +02:00
* feat: bind shared-code review advice to source and branch * feat: add shared-code extraction audit and scoped review checks * test: recognize complete source reads and explicit coverage legends * chore: bump version and changelog (v1.88.0.0) Co-Authored-By: OpenAI Codex <noreply@openai.com> * test: capture native review questions and retain public evidence Capture the actual first public native question with strict ownership and display matching. Preserve terminal failures and raw evidence, and retain SDK completion checks. * test: recognize verified review evidence and complete fixtures Recognize complete source and diagram evidence, concrete design and developer-experience decisions, and the complete planted scenario contracts. Preserve negative controls and grading thresholds. * fix: preserve decision brief structure in native questions Keep the required pros-and-cons heading and final Net field in native question text. Regenerate host outputs and document the release and evaluation repairs. Co-Authored-By: OpenAI Codex <noreply@openai.com> * docs: update project documentation for v1.88.0.0 Co-Authored-By: OpenAI Codex <noreply@openai.com> * fix: correct eval retry accounting and ship workflow gates * fix: capture native eval evidence and stabilize CI fixtures * fix: keep shared-code eval skips read-only Choose explicit no-change answers instead of mixed fix/preservation options. Reuse the bounded revalidation prompt for path fixtures so required review metadata is available without repeated discovery. Preserve source checks, retry limits, and failed native terminal outcomes. Add captured-question and callback regressions, plus evaluation selection coverage for the affected fixtures. --------- Co-authored-by: OpenAI Codex <noreply@openai.com>
125 lines
5.4 KiB
TypeScript
125 lines
5.4 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
import { readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { skillCensus } from './helpers/skill-census';
|
|
|
|
/**
|
|
* Aggregate discovery-surface budget: the sum of every skill's frontmatter
|
|
* `name` + `description` is what EVERY host loads at discovery, every session.
|
|
*
|
|
* This is the missing enforcement layer over the existing catalog-trim
|
|
* mechanism: `applyCatalogTrim` in scripts/gen-skill-docs.ts (~line 865)
|
|
* shapes each description, and the 160KB per-file warn (~line 1015) covers
|
|
* BODY size — neither caps the aggregate frontmatter the catalog is made of.
|
|
*
|
|
* Import-free by design: parses skills' SKILL.md frontmatter directly. Do not
|
|
* import gen-skill-docs internals here — this test must survive generator
|
|
* refactors.
|
|
*
|
|
* Budget derivation (re-derive it, do not trust the number):
|
|
* ref this commit
|
|
* method for each authored skill (test/helpers/skill-census.ts
|
|
* authoredSkills — symlink-deduped, root router excluded) plus the
|
|
* root router's `_gstack-command` alias frontmatter as one separate
|
|
* line item, run parseFrontmatter() below and sum
|
|
* Buffer.byteLength(name) + Buffer.byteLength(description);
|
|
* token-equivalents = ceil(bytes / 4).
|
|
* ref deslop-shared-libs addition on base a6b3a575 (2026-09-16)
|
|
* result pre-addition aggregate 4,593 bytes; deslop-shared-libs adds
|
|
* 82 bytes (name + concise description), yielding 4,675 bytes
|
|
* = 1,169 token-equivalents including the root router alias.
|
|
* New-skill ratchet: previous ceiling 1,150 + ceil(82 / 4) = 1,171
|
|
* token-equivalents (4,684 bytes), leaving 9 bytes. Existing descriptions
|
|
* are unchanged. Dominant skill: design-consultation at 229 bytes.
|
|
*/
|
|
const CATALOG_BUDGET_TOKEN_EQUIVALENTS = 1_171;
|
|
|
|
// Largest today: design-consultation at 229 bytes. A description that needs
|
|
// more than 260 bytes is a body paragraph, not a catalog entry.
|
|
const PER_SKILL_BYTE_CAP = 260;
|
|
|
|
const RATCHET_PROTOCOL =
|
|
'Adding a skill? Re-measure with: bun test test/catalog-budget.test.ts ' +
|
|
'(the failure prints the new total). Update CATALOG_BUDGET_TOKEN_EQUIVALENTS ' +
|
|
'AND the derivation comment (ref/date/value/which skill moved it) in the ' +
|
|
'SAME commit. Growing an existing description? Trim it instead — the ' +
|
|
'catalog is what every host loads at discovery, every session.';
|
|
|
|
const ROOT = join(import.meta.dir, '..');
|
|
|
|
function parseFrontmatter(body: string): { name: string; description: string } {
|
|
const name = body.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? '';
|
|
// Folded block scalar (description: >-) with two-space-indented continuation
|
|
// lines, falling back to a single-line description.
|
|
const folded = body.match(/^description:\s*>-?\r?\n((?: .*\r?\n)+)/m)?.[1];
|
|
const description = folded
|
|
? folded.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).join(' ')
|
|
: body.match(/^description:\s*(?!>-?\s*$)(.+)$/m)?.[1]?.trim() ?? '';
|
|
return { name, description };
|
|
}
|
|
|
|
interface CatalogEntry {
|
|
skill: string;
|
|
name: string;
|
|
description: string;
|
|
bytes: number;
|
|
}
|
|
|
|
function catalogEntries(): CatalogEntry[] {
|
|
const entries: CatalogEntry[] = [];
|
|
for (const skill of skillCensus(ROOT).authoredSkills) {
|
|
const body = readFileSync(join(ROOT, skill, 'SKILL.md'), 'utf8');
|
|
const { name, description } = parseFrontmatter(body);
|
|
entries.push({
|
|
skill,
|
|
name,
|
|
description,
|
|
bytes: Buffer.byteLength(name) + Buffer.byteLength(description),
|
|
});
|
|
}
|
|
// The root SKILL.md is a router, registered by setup as the
|
|
// `_gstack-command` alias — not an authored skill, but its frontmatter
|
|
// still ships in the catalog, so it counts as one line item.
|
|
const router = parseFrontmatter(readFileSync(join(ROOT, 'SKILL.md'), 'utf8'));
|
|
if (router.name && router.description) {
|
|
entries.push({
|
|
skill: '(root router)',
|
|
name: router.name,
|
|
description: router.description,
|
|
bytes: Buffer.byteLength(router.name) + Buffer.byteLength(router.description),
|
|
});
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
describe('catalog discovery-surface budget', () => {
|
|
test(`aggregate frontmatter stays within ${CATALOG_BUDGET_TOKEN_EQUIVALENTS} token-equivalents`, () => {
|
|
const entries = catalogEntries();
|
|
const totalBytes = entries.reduce((sum, e) => sum + e.bytes, 0);
|
|
const estimatedTokens = Math.ceil(totalBytes / 4);
|
|
const delta = estimatedTokens - CATALOG_BUDGET_TOKEN_EQUIVALENTS;
|
|
expect(
|
|
estimatedTokens,
|
|
`Catalog is ${estimatedTokens} token-equivalents (${totalBytes} bytes), ` +
|
|
`${delta} over the ${CATALOG_BUDGET_TOKEN_EQUIVALENTS} budget. ${RATCHET_PROTOCOL}`
|
|
).toBeLessThanOrEqual(CATALOG_BUDGET_TOKEN_EQUIVALENTS);
|
|
});
|
|
|
|
test(`every skill's name + description stays under ${PER_SKILL_BYTE_CAP} bytes`, () => {
|
|
for (const entry of catalogEntries()) {
|
|
expect(
|
|
entry.bytes,
|
|
`${entry.skill}: name + description is ${entry.bytes} bytes, ` +
|
|
`${entry.bytes - PER_SKILL_BYTE_CAP} over the ${PER_SKILL_BYTE_CAP}-byte ` +
|
|
`per-skill cap. ${RATCHET_PROTOCOL}`
|
|
).toBeLessThanOrEqual(PER_SKILL_BYTE_CAP);
|
|
}
|
|
});
|
|
|
|
test('every skill has a non-empty description', () => {
|
|
for (const entry of catalogEntries()) {
|
|
expect(entry.description, `${entry.skill}: empty or missing frontmatter description`).not.toBe('');
|
|
}
|
|
});
|
|
});
|