mirror of
https://github.com/conorbronsdon/avoid-ai-writing.git
synced 2026-09-19 01:32:11 +08:00
788c6675c1
* feat: seed OpenAI plugin skill resources * feat: add OpenAI plugin manifest and public metadata * feat: add routed ChatGPT and Codex skill graph * test: add OpenAI plugin validation and submission pack * fix: correct listing character-count evidence * feat: add typed cross-skill handoff contract * docs: encode agency review lenses for orchestration * feat: add machine-validated typed skill graph * feat: upgrade router to typed orchestration protocol * feat: connect detector to typed handoff network * feat: connect rewriter to verification and representation guards * feat: add typed file-edit handoffs and repair loop * feat: make preservation verifier a blocking typed gate * feat: connect interpretation guardrail to evidence handoffs * refactor: replace legacy yaml graph with validated json graph * test: validate skill graph and cross-skill contracts * ci: validate cross-skill orchestration graph * refactor: package connection validator inside router skill * refactor: keep connection validator inside packaged router skill * ci: run packaged connection validator before and after extraction * docs: align routing matrix with typed graph * test: expand routing evals for connected skill graph * fix: remove unintended reviewer detector cycle * fix: make interpretation reviewer terminal in skill graph * test: reject terminal outgoing edges and unbounded graph cycles * docs: align handoff contract with terminal reviewer semantics * docs: route fresh interpretation evidence through router * docs: document graph v2 and connection validation * fix: route fresh detector requests through orchestrator * test: enforce agency lenses and router-return semantics * test: cover terminal reviewer return-to-router flow * docs: add connected skill graph v2 to release notes * fix: remove unsupported skill policy products * fix: remove unsupported skill policy products * fix: remove unsupported skill policy products * fix: remove unsupported skill policy products * fix: remove unsupported skill policy products * fix: remove unsupported skill policy products * fix: remove unsupported skill policy products * fix: align plugin validator with graph v2 and ingestion policy * fix: synchronize bundled detector and verifier resources * fix: bundle detector beside preservation validator * ci: watch canonical detector resources for plugin drift * test: smoke packaged preservation residual detector * ci: exercise residual detector before and after packaging * fix(port): sync ChatGPT/Codex plugin version with canonical SKILL.md The merge with main moves the canonical skill to 3.28.0; the plugin manifest, listing, and submission pack still carried 3.25.1, so scripts/sync-plugin-skill.sh and scripts/validate-openai-plugin.py both exit 1 on the effective merge. Bump the three manifests and regenerate the copies the sync script owns. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(port): publish the plugin from the canonical repository Every public destination in the ChatGPT/Codex package pointed at the contributor's fork: websiteURL, privacyPolicyURL, termsOfServiceURL, homepage, repository, the submission listing, and SUPPORT.md. After a merge the canonical owner would not control the published legal and support pages. Point them all at conorbronsdon/avoid-ai-writing and make developerName/packagePublisher the identity that will be verified in the OpenAI portal. Packaging credit stays: NOTICE.md and listing.json now name the contributor explicitly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(detector): CRLF input no longer bypasses the preservation validator validate() extracts frontmatter, tables and headings with regexes that anchor on a bare newline. A CRLF document matched none of them, so changing 'title: one' to 'title: two' in Windows-formatted frontmatter returned { ok: true, errors: [] }. This is a pre-existing bug in the canonical detector, not something PR #135 introduced - the bundled plugin copy is byte-identical to detector/validate.js - so the fix and the must-fire fixtures go in the canonical source and the generated copy is re-synced from it. Adds four CRLF fixtures to detector/validate.test.js; all four fail against the unpatched validator. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(port): make the manifest the only source for listing metadata submission/listing.json restates twelve manifest fields and only three of them were asserted, so the listing could ship a different developer name, support URL or description than the manifest it claims to describe - which is exactly the drift that put a fork's URLs into the public package. Assert every duplicated field against .codex-plugin/plugin.json, derive the hand-maintained character counts instead of trusting them, and require every public URL to sit under the canonical author.url so publication cannot move off this repository without turning the check red. Also: guard the canonical-copy comparison against a missing plugin copy (it raised FileNotFoundError instead of reporting), scope the hygiene scan to the packaged surface instead of walking .git, and restore the CLAUDE.md path-filter rationale the port deleted from the workflow. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(port): bundle every resource the canonical skill references OpenAI's submission contract says each skill must include any referenced scripts, templates, or assets, and the Codex skill layout has no shared- resource mechanism, so skills/avoid-ai-writing/SKILL.md (byte-identical to root SKILL.md) now ships with synced copies of detector/patterns.js, detector/validate.js, detector/CATEGORIES.md, scripts/check-style.js, and examples/ at the same relative paths. scripts/sync-plugin-skill.sh produces them and the existing regenerate-then-diff CI gate covers them. A new smoke test runs check-style and the preservation validator from inside the bundled tree and from an extracted package. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(port): manifest supportURL, ancestry-checked baseCommit, policy.products interface.supportURL now lives in .codex-plugin/plugin.json and the listing value is validated against it. Both submission files record baseCommit as the origin/main merge-base and the validator checks it is an ancestor of HEAD so it cannot drift silently; the verifier's copied patterns.js joins the deterministic-resource inventory. The validator accepts policy.products with CHAT and/or CODEX per the current agents/openai.yaml schema and keeps rejecting unknown policy keys, with tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(port): follow main to 3.29.0 after #128 landed Bump .codex-plugin/plugin.json and the submission metadata to 3.29.0, set baseCommit to the new origin/main merge-base, and regenerate the bundled copies so the sync gate is clean on the merge ref. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(port): changelog and README for the plugin package; harden packager and validator Qodo re-review items: a dated 3.29.0 changelog entry and a README section for the native ChatGPT/Codex package; the packager and validator now reject symlinked top-level package files; JSON loading rejects a non-object root with a validation error instead of crashing, with tests for list and null roots and for the symlink case (skipped where symlinks need privileges). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(port): packager and validator hardening, lone-CR preservation, workflow paths Qodo re-review items 6-11: the marketplace policy no longer demands install-time authentication for a skills-only package (ON_USE, the only other documented value); the packager rejects an output path that is a packaged input or sits inside an included directory and fails on a missing top-level package file instead of silently omitting it; the validator type-checks nested submission objects before access; the preservation validator normalizes CRLF only, so a lone carriage return changed inside code is reported again; LICENSE joins the workflow path filters. Tests for each; bundled validator copies re-synced. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(port): marketplace authentication policy is first-use, not install-time The skills-only package has no authentication flow or hosted service. The marketplace docs say policy.authentication decides whether auth happens on install or first use; first use never triggers for local skills, so ON_USE replaces ON_INSTALL. (Qodo item 6, deferred from the previous commit because the implementer sandbox could not write .agents/.) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(port): graph validator rejects malformed JSON cleanly; canonical URL is a constant Qodo items 8 and 9. validate_connections.py now checks the graph root and edge field types and reports through its validation path instead of raising, with a test script wired into CI. validate-openai-plugin.py pins CANONICAL_PROJECT_URL and requires author.url to equal it, so changing every public URL to a fork no longer passes; test added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(port): bound SVG parsing and stop empty skill names polluting the graph set Qodo items 19 and 20. The validator rejects SVGs carrying <!DOCTYPE or <!ENTITY declarations and anything over 256 KiB before handing text to ElementTree, closing the entity-expansion path; a skill whose frontmatter lacks a name reports that error once instead of inserting an empty name that cascades into router-graph errors. Tests for both. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ci(port): full-history checkout so the baseCommit ancestry check can run The validator's merge-base --is-ancestor check needs the base commit in the checkout; actions/checkout's default depth of 1 made CI report it as "not a valid commit name". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Conor Bronsdon <120674402+conorbronsdon@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
258 lines
11 KiB
JavaScript
258 lines
11 KiB
JavaScript
/**
|
|
* Avoid AI Writing — preservation validator tests
|
|
*
|
|
* Two failure classes are tested, and the second matters more:
|
|
*
|
|
* 1. The validator catches destructive rewrites (code edited, quote
|
|
* reworded, URL dropped).
|
|
* 2. The validator does NOT fire on edits this skill documents as correct
|
|
* (stripping `utm_source=chatgpt.com`, sentence-casing a Title Case
|
|
* heading, re-aligning a table). A validator that blocks the skill's own
|
|
* instructions would be turned off within a day.
|
|
*
|
|
* Dependency-free; runs on node >= 18.
|
|
*/
|
|
|
|
const assert = require('node:assert/strict');
|
|
const { validate, formatResult } = require('./validate.js');
|
|
|
|
let failed = 0;
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` ✓ ${name}`);
|
|
} catch (err) {
|
|
failed++;
|
|
console.error(` ✗ ${name}`);
|
|
console.error(` ${err.message}`);
|
|
}
|
|
}
|
|
|
|
const codes = (result) => result.errors.map((e) => e.code);
|
|
const warnCodes = (result) => result.warnings.map((w) => w.code);
|
|
|
|
console.log('\npreservation validator\n');
|
|
|
|
// ── Destructive edits must fail ────────────────────────────────────────
|
|
|
|
test('fenced code edited → error', () => {
|
|
const before = 'Intro.\n\n```js\nconst x = 1;\n```\n\nOutro.';
|
|
const after = 'Intro.\n\n```js\nconst x = 2;\n```\n\nOutro.';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.equal(r.ok, false);
|
|
assert.ok(codes(r).includes('code-block-modified'));
|
|
});
|
|
|
|
test('fenced code removed → error', () => {
|
|
const before = 'Intro.\n\n```sh\nnpm test\n```\n\nOutro.';
|
|
const after = 'Intro.\n\nOutro.';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.ok(codes(r).includes('code-block-count'));
|
|
});
|
|
|
|
test('blockquote reworded → error', () => {
|
|
const before = 'He said:\n\n> The system is slow and it is getting slower.\n> We need to fix it.\n\nThat is the claim.';
|
|
const after = 'He said:\n\n> The system is slow and getting slower.\n> We need to fix it.\n\nThat is the claim.';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.ok(codes(r).includes('blockquote-modified'));
|
|
});
|
|
|
|
test('table cell content changed → error', () => {
|
|
const before = '| Repo | Stars |\n|---|---|\n| patina | 278 |\n';
|
|
const after = '| Repo | Stars |\n|---|---|\n| patina | 280 |\n';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.ok(codes(r).includes('table-modified'));
|
|
});
|
|
|
|
test('inline code dropped → error', () => {
|
|
const before = 'Run `npm test` before pushing the change.';
|
|
const after = 'Run the test suite before pushing the change.';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.ok(codes(r).includes('inline-code-missing'));
|
|
});
|
|
|
|
test('URL dropped → error', () => {
|
|
const before = 'See https://example.com/report for the numbers.';
|
|
const after = 'See the linked report for the numbers.';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.ok(codes(r).includes('url-missing'));
|
|
});
|
|
|
|
test('file path altered → error', () => {
|
|
const before = 'The engine lives in ./detector/patterns.js today.';
|
|
const after = 'The engine lives in ./detector/patterns.ts today.';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.ok(codes(r).includes('path-missing'));
|
|
});
|
|
|
|
test('section deleted → heading count error', () => {
|
|
const before = '# Title\n\nBody text here.\n\n## Second\n\nMore body text.';
|
|
const after = '# Title\n\nBody text here.';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.ok(codes(r).includes('heading-count'));
|
|
});
|
|
|
|
test('heading nesting changed → error', () => {
|
|
const before = '# Title\n\nBody.\n\n## Sub\n\nMore.';
|
|
const after = '# Title\n\nBody.\n\n### Sub\n\nMore.';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.ok(codes(r).includes('heading-level'));
|
|
});
|
|
|
|
test('frontmatter touched → error', () => {
|
|
const before = '---\ntitle: Draft\n---\n\nBody.';
|
|
const after = '---\ntitle: Draft v2\n---\n\nBody.';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.ok(codes(r).includes('frontmatter-modified'));
|
|
});
|
|
|
|
// ── CRLF input must not bypass the extractors ────────────────────
|
|
//
|
|
// The extractors anchor on \n. A Windows-authored document reaches this
|
|
// validator with CRLF, and before validate() normalized line endings these
|
|
// three cases all returned ok:true with an empty errors array - protected
|
|
// frontmatter and table cells could be rewritten silently. These must fire.
|
|
|
|
const crlf = (s) => s.replace(/\n/g, '\r\n');
|
|
|
|
test('CRLF frontmatter touched -> error', () => {
|
|
const before = crlf('---\ntitle: one\n---\n\nBody.\n');
|
|
const after = crlf('---\ntitle: two\n---\n\nBody.\n');
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.equal(r.ok, false, formatResult(r));
|
|
assert.ok(codes(r).includes('frontmatter-modified'), formatResult(r));
|
|
});
|
|
|
|
test('CRLF frontmatter removed -> error', () => {
|
|
const before = crlf('---\ntitle: one\n---\n\nBody.\n');
|
|
const after = crlf('Body.\n');
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.ok(codes(r).includes('frontmatter-modified'), formatResult(r));
|
|
});
|
|
|
|
test('CRLF table cell changed -> error', () => {
|
|
const before = crlf('Intro.\n\n| a | b |\n|---|---|\n| 1 | 2 |\n\nOutro.\n');
|
|
const after = crlf('Intro.\n\n| a | b |\n|---|---|\n| 1 | 9 |\n\nOutro.\n');
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.equal(r.ok, false, formatResult(r));
|
|
});
|
|
|
|
test('CRLF and LF forms of the same document compare clean', () => {
|
|
const lf = '---\ntitle: one\n---\n\n# Heading\n\nBody.\n';
|
|
const r = validate(lf, crlf(lf), { skipResidual: true });
|
|
assert.equal(r.ok, true, formatResult(r));
|
|
});
|
|
|
|
test('lone CR changed to LF inside fenced code -> error', () => {
|
|
const before = '```text\nline one\rline two\n```';
|
|
const after = '```text\nline one\nline two\n```';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.equal(r.ok, false, formatResult(r));
|
|
assert.ok(codes(r).includes('code-block-modified'), formatResult(r));
|
|
});
|
|
|
|
// ── Documented, correct edits must pass ────────────────────────────────
|
|
|
|
test('stripping an AI utm_source parameter → no error', () => {
|
|
const before = 'Background: https://example.com/post?utm_source=chatgpt.com and more.';
|
|
const after = 'Background: https://example.com/post and more.';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.equal(r.ok, true, formatResult(r));
|
|
});
|
|
|
|
test('sentence-casing a Title Case heading → warning, not error', () => {
|
|
const before = '## Strategic Negotiations And Key Partnerships\n\nBody text goes here.';
|
|
const after = '## Strategic negotiations and key partnerships\n\nBody text goes here.';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.equal(r.ok, true, formatResult(r));
|
|
assert.ok(warnCodes(r).includes('heading-text'));
|
|
});
|
|
|
|
test('removing an emoji from a heading → warning, not error', () => {
|
|
const before = '## 🚀 What this means\n\nBody text goes here.';
|
|
const after = '## What this means\n\nBody text goes here.';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.equal(r.ok, true, formatResult(r));
|
|
});
|
|
|
|
test('re-aligning table padding → no error', () => {
|
|
const before = '| Repo | Stars |\n|---|---|\n| patina | 278 |\n';
|
|
const after = '| Repo | Stars |\n| ------ | ----- |\n| patina | 278 |\n';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.equal(r.ok, true, formatResult(r));
|
|
});
|
|
|
|
test('ordinary prose rewrite → no error', () => {
|
|
const before = 'We leverage a robust and comprehensive framework to delve into the landscape of developer tooling.';
|
|
const after = 'We use a reliable framework to look at developer tooling.';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.equal(r.ok, true, formatResult(r));
|
|
});
|
|
|
|
test('four-space list continuation is not treated as code', () => {
|
|
const before = '- First item\n\n continued paragraph under the item\n\n- Second item';
|
|
const after = '- First item\n\n continued paragraph under the item\n\n- Second item';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.equal(r.stats.indentedBlocks, 0);
|
|
});
|
|
|
|
test('URL inside a code block is not double-counted as prose', () => {
|
|
const before = '```sh\ncurl https://example.com/api\n```\n\nPlain body text.';
|
|
const after = '```sh\ncurl https://example.com/api\n```\n\nPlain body copy.';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.equal(r.ok, true, formatResult(r));
|
|
});
|
|
|
|
// ── Warnings ──────────────────────────────────────────────────────────
|
|
|
|
test('dropped figure → warning', () => {
|
|
const before = 'Revenue grew 42% last quarter on 1,200 new accounts.';
|
|
const after = 'Revenue grew sharply last quarter on 1,200 new accounts.';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.equal(r.ok, true);
|
|
assert.ok(warnCodes(r).includes('number-missing'));
|
|
});
|
|
|
|
test('halved text → shrink warning', () => {
|
|
const before = Array.from({ length: 60 }, (_, i) => `word${i}`).join(' ') + '.';
|
|
const after = Array.from({ length: 20 }, (_, i) => `word${i}`).join(' ') + '.';
|
|
const r = validate(before, after, { skipResidual: true });
|
|
assert.ok(warnCodes(r).includes('large-shrink'));
|
|
});
|
|
|
|
// ── Residual patterns ─────────────────────────────────────────────────
|
|
|
|
test('rewrite that adds AI patterns → residual error', () => {
|
|
const before = 'The team shipped the parser on Tuesday. It reads 40MB files without the old memory spike.';
|
|
const after = 'The team leveraged a robust, comprehensive approach to delve into the landscape of parsing. '
|
|
+ 'It is important to note that this is a testament to their seamless, cutting-edge paradigm. '
|
|
+ 'Great question! I hope this helps!';
|
|
const r = validate(before, after);
|
|
assert.equal(r.ok, false);
|
|
assert.ok(codes(r).includes('residual-grew'), formatResult(r));
|
|
});
|
|
|
|
test('rewrite that removes AI patterns → clean', () => {
|
|
const before = 'We leverage a robust and comprehensive framework to delve into the ever-evolving landscape '
|
|
+ 'of developer tooling, showcasing a seamless and cutting-edge paradigm. It is important to note that '
|
|
+ 'this is a testament to the team.';
|
|
const after = 'We use a reliable framework to study developer tooling. The team built it in six weeks.';
|
|
const r = validate(before, after);
|
|
assert.equal(r.ok, true, formatResult(r));
|
|
assert.ok(r.stats.residual.issuesAfter <= r.stats.residual.issuesBefore);
|
|
});
|
|
|
|
test('identical input and output is always clean', () => {
|
|
const text = '---\ntitle: Test\n---\n\n# Heading\n\nBody with `code`, a https://example.com link, and ./a/path.js.\n\n> A quote.\n> Second line.\n\n| a | b |\n|---|---|\n| 1 | 2 |\n';
|
|
const r = validate(text, text);
|
|
assert.equal(r.ok, true, formatResult(r));
|
|
assert.equal(r.warnings.length, 0, formatResult(r));
|
|
});
|
|
|
|
test('non-string input throws', () => {
|
|
assert.throws(() => validate('a', null), TypeError);
|
|
});
|
|
|
|
console.log(`\n${failed === 0 ? 'all preservation tests passed' : `${failed} test(s) failed`}\n`);
|
|
process.exit(failed === 0 ? 0 : 1);
|