mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-19 00:00:13 +02:00
ab66193e2e
Carve the largest skill (138,838 B) into a skeleton + one on-demand
section, the documented next Phase B target after /ship (v2_PLAN.md:216).
- sections/review-sections.md(.tmpl): the 11-section deep review, codex/
outside-voice rules, how-to-ask, Required Outputs, registries, Completion
Summary, Review Log, REVIEW_DASHBOARD, PLAN_FILE_REVIEW_REPORT, Next Steps,
docs/designs promotion, Formatting Rules, and the Mode Quick Reference.
- sections/manifest.json: passive registry (CM2), one entry.
- SKILL.md.tmpl: {{SECTION_INDEX}} after the system audit, a single
{{SECTION:review-sections}} STOP-Read after Step 0 mode selection, and a
Section self-check. All of Step 0 (the scope/mode conversation) stays in
the always-loaded skeleton; only EXIT_PLAN_MODE_GATE follows the section.
Measured: always-loaded skeleton 138,838 -> 80,731 B (-42%, ~14.4K tokens
off every invocation). Union (skeleton + section) 139,110 B, behavior held.
Boundary honors Codex P1: nothing review-governing (formatting rules, mode
reference, how-to-ask, required outputs) sits in the skeleton below the
STOP. Housekeeping resolvers ride in the section, matching the ship
precedent (adversarial.md carries LEARNINGS_LOG + GBRAIN_SAVE_RESULTS).
Tests (atomic with the carve — skill-docs.yml gates gen:skill-docs
freshness on every push, so source + regen + tests must land together):
- parity-harness: plan-ceo flipped to sectioned, maxSkeletonBytes 90_000
(measured 80,731 + headroom); content/minBytes run against the union.
- skill-size-budget: plan-ceo-review added to SECTIONS_EXTRACTED.
- section-manifest-consistency: generalized to discover every carved skill,
vars computed per-skill-case (Codex P2).
- skill-ceo-section-ordering (new, gate): per-PR static guard — STOP after
Step 0, review body absent from skeleton, report writer in the section,
nothing review-governing below the STOP.
- skill-e2e-plan-ceo-review-section-loading (new, periodic): refreshes the
installed skill first (Codex P1), drives full Step 0, asserts the section
is Read before the report.
- gen-skill-docs + skill-validation: read the skeleton+sections union for
carved skills so relocated prose still counts.
- touchfiles: plan-ceo-section-loading registered (periodic).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
109 lines
4.8 KiB
TypeScript
109 lines
4.8 KiB
TypeScript
/**
|
|
* Section manifest ↔ filesystem consistency (v2 plan T9 / Phase C orphan check).
|
|
*
|
|
* Implements the 3-tier orphan classification from v2_PLAN.md:
|
|
* - generated orphan (sections/X.md with no sections/X.md.tmpl) → FAIL
|
|
* - hand-edited generated file (X.md missing the AUTO-GENERATED header) → FAIL
|
|
* - manifest orphan (sections/X.md.tmpl not listed in manifest) → WARN (v2.0)
|
|
*
|
|
* Also pins the PASSIVE-manifest contract (CM2 / v2_PLAN.md:663): manifest entries
|
|
* carry only id/file/title/trigger — no machine predicate (applies_when/required_for).
|
|
*
|
|
* Generalized for every carved skill (v2 plan Phase B). Carved skills are
|
|
* discovered dynamically (any top-level dir with sections/manifest.json), so a new
|
|
* carve is covered the moment its manifest lands — no edit here. Per Codex
|
|
* outside-voice P2, each skill's manifest + dir listing is read INSIDE its own
|
|
* describe case (not at module top), so a carve-in-progress (manifest added before
|
|
* the .md is generated) fails only that skill's generated-.md assertion instead of
|
|
* crashing the whole module, and the suite never silently stays ship-only.
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
|
|
/** Every top-level skill dir that owns a sections/manifest.json. */
|
|
function discoverCarvedSkills(): string[] {
|
|
return fs
|
|
.readdirSync(ROOT, { withFileTypes: true })
|
|
.filter(d => d.isDirectory())
|
|
.map(d => d.name)
|
|
.filter(name => fs.existsSync(path.join(ROOT, name, 'sections', 'manifest.json')))
|
|
.sort();
|
|
}
|
|
|
|
const CARVED_SKILLS = discoverCarvedSkills();
|
|
|
|
describe('section manifest ↔ filesystem consistency', () => {
|
|
test('the known carved skills are discovered', () => {
|
|
// Tripwire: if a carve regresses (manifest deleted) this catches it.
|
|
expect(CARVED_SKILLS).toContain('ship');
|
|
expect(CARVED_SKILLS).toContain('plan-ceo-review');
|
|
});
|
|
|
|
for (const skill of CARVED_SKILLS) {
|
|
describe(skill, () => {
|
|
// Codex P2: computed per-skill-case, not at module load.
|
|
const sectionsDir = path.join(ROOT, skill, 'sections');
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(sectionsDir, 'manifest.json'), 'utf-8'));
|
|
const sectionTmpls = fs.readdirSync(sectionsDir).filter(f => f.endsWith('.md.tmpl'));
|
|
const sectionMds = fs.readdirSync(sectionsDir).filter(f => f.endsWith('.md') && !f.endsWith('.md.tmpl'));
|
|
|
|
test('manifest parses with skill + sections array', () => {
|
|
expect(manifest.skill).toBe(skill);
|
|
expect(Array.isArray(manifest.sections)).toBe(true);
|
|
expect(manifest.sections.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('every manifest entry has a .md.tmpl source AND a generated .md', () => {
|
|
for (const s of manifest.sections) {
|
|
expect(fs.existsSync(path.join(sectionsDir, `${s.file}.tmpl`))).toBe(true);
|
|
expect(fs.existsSync(path.join(sectionsDir, s.file))).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('manifest is PASSIVE — no applies_when / required_for predicate (CM2)', () => {
|
|
for (const s of manifest.sections) {
|
|
expect(s).not.toHaveProperty('applies_when');
|
|
expect(s).not.toHaveProperty('required_for');
|
|
// The allowed passive shape:
|
|
expect(typeof s.id).toBe('string');
|
|
expect(typeof s.file).toBe('string');
|
|
expect(typeof s.title).toBe('string');
|
|
expect(typeof s.trigger).toBe('string');
|
|
}
|
|
});
|
|
|
|
test('no generated orphan: every sections/X.md has a sections/X.md.tmpl → FAIL', () => {
|
|
const orphans = sectionMds.filter(md => !sectionTmpls.includes(`${md}.tmpl`));
|
|
expect(orphans).toEqual([]);
|
|
});
|
|
|
|
test('no hand-edited generated file: every sections/X.md has the AUTO-GENERATED header → FAIL', () => {
|
|
for (const md of sectionMds) {
|
|
const head = fs.readFileSync(path.join(sectionsDir, md), 'utf-8').slice(0, 120);
|
|
expect(head).toContain('AUTO-GENERATED');
|
|
}
|
|
});
|
|
|
|
test('manifest orphan check (WARN in v2.0): every .md.tmpl is listed', () => {
|
|
const listed = new Set(manifest.sections.map((s: { file: string }) => `${s.file}.tmpl`));
|
|
const unlisted = sectionTmpls.filter(t => !listed.has(t));
|
|
if (unlisted.length > 0) {
|
|
// v2_PLAN.md: WARN now, FAIL in v2.1. Surface, don't fail the build yet.
|
|
// eslint-disable-next-line no-console
|
|
console.warn(`[section-manifest] ${skill} manifest orphan(s) (not in manifest.json): ${unlisted.join(', ')}`);
|
|
}
|
|
expect(unlisted.length).toBeLessThanOrEqual(unlisted.length); // always passes; WARN only
|
|
});
|
|
|
|
test('section ids are unique', () => {
|
|
const ids = manifest.sections.map((s: { id: string }) => s.id);
|
|
expect(new Set(ids).size).toBe(ids.length);
|
|
});
|
|
});
|
|
}
|
|
});
|