Files
gstack/test/helpers/docsync-contract.ts
Garry Tanandcapy-ai[bot] 28d0fa8c51 v1.93.2.0 GSTA-240 follow-up: native cookie cleanup waits for Edge to exit, plus three flaky-test repairs (#3805)
* fix(flaky): native cookie cleanup waits for exit, Sonnet guard actor, 30 s local pregate lane, receipt write timeout (GSTA-240 follow-up)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(flaky): coverage-audit wc -c and bounded-display credit, tpa-broken Aside absence, register docsync fixture (GSTA-240 follow-up)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: register the GSTA-240 follow-up fixtures in FREE_FIXTURES

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: real-Edge test bounds follow NATIVE_CLEANUP_MS; Aside absence discounted only beside gstack's own browser (review pass 1)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: drop the Sonnet actor and the grep|head and Aside-absence loosenings (review round 2), parent kill timer gets 2 s over the cleanup budget

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-11-02 15:08:48 +00:00

108 lines
6.4 KiB
TypeScript

import * as path from 'node:path';
export interface DocsCompletion {
schema_version: 1;
audit_id: string;
status: 'updated' | 'current' | 'blocked';
files_updated: string[];
files_reviewed: string[];
documentation_section: string;
blockers: string[];
decisions: string[];
}
const keys = ['schema_version', 'audit_id', 'status', 'files_updated', 'files_reviewed', 'documentation_section', 'blockers', 'decisions'];
export function parseDocsCompletion(output: string, auditId: string): DocsCompletion {
const result = JSON.parse(output.trimEnd().split('\n').at(-1)!);
if (!result || Array.isArray(result) || typeof result !== 'object') throw new Error('completion fields');
const missing = keys.filter(key => !Object.hasOwn(result, key)), extra = Object.keys(result).filter(key => !keys.includes(key));
if (missing.length || extra.length) throw new Error(`completion fields: missing ${missing.join(',') || 'none'}; extra ${extra.join(',') || 'none'}`);
if (result.schema_version !== 1 || result.audit_id !== auditId) throw new Error('completion identity');
if (!['updated', 'current', 'blocked'].includes(result.status)) throw new Error('completion status');
for (const field of ['files_updated', 'files_reviewed', 'blockers', 'decisions']) {
if (!Array.isArray(result[field]) || result[field].some((v: unknown) => typeof v !== 'string' || !v.trim())) {
throw new Error(`completion ${field}`);
}
}
if (typeof result.documentation_section !== 'string' || !result.documentation_section.trim()) throw new Error('completion markdown');
for (const field of ['files_updated', 'files_reviewed']) {
const paths: string[] = result[field];
if (new Set(paths).size !== paths.length || paths.some(p => path.posix.isAbsolute(p) || p.includes('\\') ||
p.split('/').some(part => !part || part === '.' || part === '..') || /[*?\[\]]/.test(p))) throw new Error('completion paths');
}
if (result.status === 'blocked' ? result.blockers.length === 0 : result.blockers.length !== 0) throw new Error('completion blockers');
if (result.status === 'current' && result.files_updated.length !== 0 ||
result.status === 'updated' && result.files_updated.length === 0) throw new Error('completion edits');
return result;
}
export function vetDocsCompletion(result: DocsCompletion, evidence: {
settled: boolean;
markerSeen: boolean;
headUnchanged: boolean;
indexUnchanged: boolean;
candidateUnchanged: boolean;
readOnly: boolean;
changedPaths: string[];
allowedDocs: string[];
}): void {
if (!evidence.settled || !evidence.markerSeen) throw new Error('unsettled or unmarked child');
if (!evidence.headUnchanged || !evidence.indexUnchanged) throw new Error('Git ownership violation');
if (!evidence.candidateUnchanged) throw new Error('stale candidate');
if (evidence.readOnly && evidence.changedPaths.length) throw new Error('read-only mutation');
if ([...evidence.changedPaths].sort().join('\0') !== [...result.files_updated].sort().join('\0')) throw new Error('unreported edits');
const forbidden = /(?:^|\/)(?:VERSION|CHANGELOG(?:\.[^/]*)?|TODOS(?:\.[^/]*)?|package(?:-lock)?\.json|[^/]*lock[^/]*|manifest\.json)$/i;
if (evidence.changedPaths.some(p => forbidden.test(p) || !evidence.allowedDocs.includes(p))) throw new Error('non-doc mutation');
}
/**
* The changed paths the accepted (last) attempt answers for. A rejected earlier attempt's reported edit stays in the tree
* (Blocked recovery preserves partial content), so it is not the accepted attempt's unreported edit; the accepted attempt
* answers for it again only when its own `files_updated` claims it (an honest re-audit that re-edited the same document).
*/
export function acceptedChangedPaths(changed: string[], contracts: Array<Pick<DocsCompletion, 'files_updated'>>): string[] {
const accepted = contracts.at(-1)!;
const retained = contracts.slice(0, -1).flatMap(earlier => earlier.files_updated);
return changed.filter(p => !retained.includes(p) || accepted.files_updated.includes(p));
}
/**
* Each dispatch's completion in order. The accepted (last) attempt must meet the whole contract. An earlier attempt is one
* the parent rejected, so only what it reported is read: its audit id, status and files_updated (gate 40805543956: attempt
* 1 omitted documentation_section, the parent rejected it and re-audited over its retained edit).
*/
export function docsAttemptContracts(outputs: string[]): { accepted: DocsCompletion; attempts: Array<Pick<DocsCompletion, 'audit_id' | 'status' | 'files_updated'>> } {
const reports = outputs.map(output => JSON.parse(output.trimEnd().split('\n').at(-1)!));
const accepted = parseDocsCompletion(outputs.at(-1)!, reports.at(-1)?.audit_id);
const earlier = reports.slice(0, -1).map(report => {
if (!report || typeof report.audit_id !== 'string' || !['updated', 'current', 'blocked'].includes(report.status) || !Array.isArray(report.files_updated)
|| report.files_updated.some((file: unknown) => typeof file !== 'string' || !file.trim())) throw new Error('rejected attempt report');
return { audit_id: report.audit_id, status: report.status, files_updated: report.files_updated };
});
return { accepted, attempts: [...earlier, accepted] };
}
export function extractDocsDispatch(section: string): string {
const begin = section.indexOf('**Subagent prompt:**');
const end = section.indexOf('**Parent processing:**');
if (begin < 0 || end <= begin) throw new Error('documentation dispatch markers moved');
return section.slice(begin + '**Subagent prompt:**'.length, end)
.split('\n').map(line => line.replace(/^> ?/, '')).join('\n').trim();
}
export function docsDispatchIndex(calls: Array<{ tool: string; input: unknown }>): number {
return docsDispatchIndices(calls)[0] ?? -1;
}
/**
* Every /document-release dispatch, in order. The audit gate's attempt budget is an initial audit plus one repair/re-audit, so a
* run has one or two; the parent's accepted contract is the last (gate 40465691803: the parent rejected attempt 1 for a false
* claim in its section, re-audited over its retained edit, and reported attempt 2's section).
*/
export function docsDispatchIndices(calls: Array<{ tool: string; input: unknown }>): number[] {
return calls.flatMap((call, index) => ['Agent', 'Task'].includes(call.tool) &&
/document-release\/SKILL\.md|executing the \/document-release workflow/i.test(JSON.stringify(call.input)) &&
!/Parent processing:|## Step 19: Create PR\/MR/.test(JSON.stringify(call.input)) ? [index] : []);
}