Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/scripts/tests/verify-no-work-records.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ def test_documentation_directories_exempt_shape_but_not_session_rule(self):
self.assertEqual(self.run_script(self.fixture(CLEAN + ["docs/Plan-roadmap.md"])).returncode, 0)
self.assertEqual(self.run_script(self.fixture(CLEAN + ["decisions/HANDOVER.md"])).returncode, 1)

def test_vendored_trees_skip_name_rules_but_not_runtime_rules(self):
vendored = ["vendor/claude-seo/extensions/banana/references/prompt-engineering.md",
"tools/vendor/lib/HANDOVER-notes.md", "vendor/PLAN-upstream.md"]
result = self.run_script(self.fixture(CLEAN + vendored))
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
for name in ["references/prompt-x.md", "Vendor/lib/prompt-x.md", "vendor-lib/prompt-x.md", ".ai-work/vendor/notes.md"]:
with self.subTest(path=name):
# Without the vendored files, so Vendor/ cannot fold into vendor/ on a case-insensitive disk.
result = self.run_script(self.fixture(CLEAN + [name]))
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertIn("violations: 1", result.stdout)

def test_extra_allowed_inputs(self):
files = CLEAN + ["LIFECYCLE.md", "product-docs/plan-coverage-audit.md", "product-docs/Plan-coverage.md"]
self.assertEqual(self.run_script(self.fixture(files)).returncode, 1)
Expand Down Expand Up @@ -225,7 +237,7 @@ def test_self_test_passes_and_breaks_on_demand(self):
with tempfile.TemporaryDirectory() as cwd:
passing = self.run_script(cwd, "--self-test")
self.assertEqual(passing.returncode, 0, passing.stdout + passing.stderr)
self.assertIn("self-test: 5 planted, 5 red, control 0, broken-listing 2", passing.stdout)
self.assertIn("self-test: 7 planted, 7 red, control 0, broken-listing 2", passing.stdout)
broken = self.run_script(cwd, "--self-test", VERIFY_SELF_TEST_BREAK="1")
self.assertNotEqual(broken.returncode, 0)

Expand Down
11 changes: 8 additions & 3 deletions .github/scripts/verify-no-work-records.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const rootNames = 'README LICENSE AGENTS CLAUDE GEMINI CODE_OF_CONDUCT CONTRIBUT
const legacySession = /(^|\/)[^/]*HANDOVER[^/]*\.md$|(^|\/)PROMPT-[^/]+\.md$|(^|\/)[^/]*NEXT-SESSION[^/]*\.md$|(^|\/)SESSION-PROMPT[^/]*\.md$|(^|\/)[^/]*SESSION[-_]HANDOVER[^/]*\.md$/;
const session = /(^|\/)(HANDOVER|PROMPT|SESSION)[-_][^/]+\.md$|(^|\/)NEXT-SESSION([-_][^/]+)?\.md$/i;
const shape = /(^|\/)(PLAN|DESIGN|REPORT|STATUS)([-_][^/]*)?\.md$/i;
// Third-party trees vendored under a directory named exactly `vendor` keep their
// upstream file names, so the name-based rules skip them.
const vendored = /(^|\/)vendor\//;
const splitInput = value => (value ?? '').split(',').map(part => part.trim()).filter(Boolean);

export function checkPaths(files, environment = process.env) {
Expand All @@ -32,7 +35,7 @@ export function checkPaths(files, environment = process.env) {
const documentation = /(^|\/)templates\//i.test(path) || docs.some(prefix => path.toLowerCase().startsWith(prefix));
if (!path.includes('/') && /^[A-Z][A-Z0-9_.-]*[A-Z0-9]\.md$/.test(path) && !allowedRoot) {
rule = 'root-document';
} else if (!/^\.(claude|github|pi|agents|cratis)\//.test(path)) {
} else if (!/^\.(claude|github|pi|agents|cratis)\//.test(path) && !vendored.test(path)) {
if (legacySession.test(path) || session.test(path)) {
rule = 'session';
} else if (shape.test(path) && !allowedRoot && !documentation) {
Expand Down Expand Up @@ -98,15 +101,17 @@ function selfTest() {
['pi-runtime', '.pi/fusion/x/prompt.md'],
['root-document', 'IMPLEMENTATION_STATUS.md'],
['session', 'decisions/hAnDoVeR-notes.md'],
['work-record-shape', 'Notes/pLaN-something.md']
['work-record-shape', 'Notes/pLaN-something.md'],
['session', 'references/prompt-x.md'],
['session', 'Vendor/lib/prompt-x.md']
];
let red = 0;
for (const [index, [rule, path]] of plants.entries()) {
const result = run(`plant-${index}`, ['README.md', path]);
if (result.status === 1 && result.stdout.includes(`[${rule}] ${JSON.stringify(path)}`) && result.stdout.includes('violations: 1')) red++;
else console.error(`self-test: planted defect not caught: ${path}`);
}
const clean = ['README.md', 'DECISIONS.md', 'decisions/D-0001-example.md', 'Documentation/decisions/0003-kernel-boundary.md', 'Source/Reporting.md', 'templates/build-kit/lib/prompt.md', 'templates/session.md', 'templates/handover.md', '.pi/settings.json'];
const clean = ['README.md', 'DECISIONS.md', 'decisions/D-0001-example.md', 'Documentation/decisions/0003-kernel-boundary.md', 'Source/Reporting.md', 'templates/build-kit/lib/prompt.md', 'templates/session.md', 'templates/handover.md', '.pi/settings.json', 'vendor/claude-seo/references/prompt-engineering.md', 'tools/vendor/lib/HANDOVER-notes.md', 'vendor/PLAN-upstream.md'];
if (process.env.VERIFY_SELF_TEST_BREAK === '1') clean.push('PLAN-break.md');
const control = run('control', clean);
const counts = control.stdout.includes(`scanned: ${clean.length} tracked files, rules: ${ruleCount}, violations: 0`);
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/verify-no-work-records.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ jobs:
fi
script="$RUNNER_TEMP/verify-no-work-records.mjs"
if ! curl --fail --silent --show-error --location --connect-timeout 10 --max-time 60 \
'https://raw.githubusercontent.com/Cratis/Workflows/366135082754c74199b284163fb00b8e596cc424/.github/scripts/verify-no-work-records.mjs' \
'https://raw.githubusercontent.com/Cratis/Workflows/c310348e882067468f8b7b517a57799cdf9a3be9/.github/scripts/verify-no-work-records.mjs' \
--output "$script"; then
echo 'could not run: downloading the pinned work-record guard failed' >&2
exit 2
fi
script_sha256='4b0628fa06fb573f8b16f4398ded1c394f2b39294d6000bb1a65ce2711387c34'
script_sha256='7f9314da92eb4764e9baad91d302d25001588836408aacc4ae5275b42cc49b38'
if ! node --input-type=module --eval '
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/verify-pull-request-policy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -863,12 +863,12 @@ jobs:
fi
script="$RUNNER_TEMP/verify-no-work-records.mjs"
if ! curl --fail --silent --show-error --location --connect-timeout 10 --max-time 60 \
'https://raw.githubusercontent.com/Cratis/Workflows/366135082754c74199b284163fb00b8e596cc424/.github/scripts/verify-no-work-records.mjs' \
'https://raw.githubusercontent.com/Cratis/Workflows/c310348e882067468f8b7b517a57799cdf9a3be9/.github/scripts/verify-no-work-records.mjs' \
--output "$script"; then
echo 'could not run: downloading the pinned work-record guard failed' >&2
exit 2
fi
script_sha256='4b0628fa06fb573f8b16f4398ded1c394f2b39294d6000bb1a65ce2711387c34'
script_sha256='7f9314da92eb4764e9baad91d302d25001588836408aacc4ae5275b42cc49b38'
if ! node --input-type=module --eval '
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1008,7 +1008,7 @@ A reusable guard that fails when AI session work records are tracked. It applies
| `session` | explicit session prefixes at any depth, case-insensitively (`Handover-*`, `PROMPT-*`, `NEXT-SESSION`, `Session-*`, with `-` or `_` separators); legacy embedded all-caps names such as `HANDOVER.md` are also caught; never exempted by a documentation directory. Bare `prompt.md`, `session.md` and `handover.md` remain ordinary product files |
| `work-record-shape` | a `PLAN`, `DESIGN`, `REPORT` or `STATUS` file at any depth (`PLAN-foo.md`, `Plan-foo.md`, `Status.md`), outside the documentation directories and product `templates/` directories |

Decision records are documentation. `decisions/`, `Documentation/`, `docs/`, `Knowledge/`, `evidence/` and `governance/` at the repository root exempt the `work-record-shape` rule, case-insensitively. Product `templates/` directories at any depth also exempt the shape rule (for example Direct's prompt sections), not the session/runtime rules. Files under `.claude/`, `.github/`, `.pi/`, `.agents/` and `.cratis/` are skipped by the name rules. Work-record prefixes match every letter case, including `pLaN-notes.md` and `design-notes.md`, outside the documentation directories.
Decision records are documentation. `decisions/`, `Documentation/`, `docs/`, `Knowledge/`, `evidence/` and `governance/` at the repository root exempt the `work-record-shape` rule, case-insensitively. Product `templates/` directories at any depth also exempt the shape rule (for example Direct's prompt sections), not the session/runtime rules. Files under `.claude/`, `.github/`, `.pi/`, `.agents/` and `.cratis/` are skipped by the name rules. Third-party trees under a directory named exactly `vendor/`, at any depth, are skipped by the name rules too, so vendored files keep their upstream names (for example `vendor/claude-seo/references/prompt-engineering.md`); the `.ai-work/` and `.pi/` runtime rules still apply. Work-record prefixes match every letter case, including `pLaN-notes.md` and `design-notes.md`, outside the documentation directories.

Inputs:

Expand Down
Loading