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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Each release of Research-Kit, newest first. The version is `KIT_VERSION` in

## Unreleased

- `path-authority` and `researcher-release` name a value flag with no value and print usage with exit 2 before resolving paths, including when another flag follows (#288).

- `property-replay --all` refuses a missing regression directory with exit 2 and
names it; an existing empty directory explicitly reports no captured regressions.
JSON results identify the directory and zero count (#307).
Expand Down
5 changes: 4 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,14 +277,17 @@ markers - they wrote `research/` into any folder until 2026-09-28, `prior` a PRI
`doctor --fix-arity` and `install` name a write the system refused - `could not write <file>:
<CODE> (why)`, exit 2, via `writeFailure` - where they printed a stack trace, 2026-09-28),
`path-authority.mjs` (offline Git-origin/path-authority snapshot validation and
schema conformance),
schema conformance; refuses a missing value for `--file`, `--schema` or `--root` by
name before reading any input, as a usage error with exit 2),
`researcher-release.mjs` (ported 2026-09-20, ADR-0029 — three subcommands over the two
validators: `validate` for R28–R32 release evidence, `conform` for schema-only checking,
`fi-validate` for an FI bundle. It **duplicates no validation**: every check comes from
`release-validator.mjs` or `fi-validator.mjs` through their public exports. Reads **no
environment variable at all**, so it cannot depend on a credential or a config. Status
maps to exit code — `PASS` 0, `FAIL`/`REOPEN` 1, `INCOMPLETE` 2, `BLOCKED` 3, unknown 4 —
and a **usage error exits 2 from every subcommand**, printing the usage block to stderr;
its argument parser names a value flag without a value before resolving any path,
including when the next token is another flag;
`test/support-policy.test.mjs` pins that by running the CLI. It writes exactly one thing,
only when asked: `fi-validate --report <file>`),
`selftest.mjs` (every requested group must match a test file before any test starts; an unmatched group is named and refused with exit 2, including beside a valid group; #332. CLI regressions isolate child report paths from the parent suite; the whole suite, queued by the harness — which **awaits** every test,
Expand Down
2 changes: 1 addition & 1 deletion research-kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ node research-kit/bin/selftest.mjs # all of it
node research-kit/bin/selftest.mjs gate hook # just these files
```

1836 tests, offline, no key and no network. The runner **awaits** every test, so `ok` means
1838 tests, offline, no key and no network. The runner **awaits** every test, so `ok` means
the assertions settled (ADR-0021), and each test is raced against a watchdog
(`RESEARCH_KIT_TEST_TIMEOUT`, default 60s).

Expand Down
4 changes: 4 additions & 0 deletions research-kit/bin/path-authority.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ function parse(argv) {
for (let i = 1; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === '--json') { result.json = true; continue; }
if (['--file', '--schema', '--root'].includes(arg)
&& (argv[i + 1] === undefined || argv[i + 1] === '' || argv[i + 1].startsWith('--'))) {
return { error: `${arg} needs a value` };
}
if (arg === '--file') { result.files.push(argv[++i]); continue; }
if (arg === '--schema') { result.schema = argv[++i]; continue; }
if (arg === '--root') { result.root = argv[++i]; continue; }
Expand Down
5 changes: 5 additions & 0 deletions research-kit/bin/researcher-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ function argsAfterCommand(argv) {
for (let i = 1; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === '--json') { result.json = true; continue; }
if (arg.startsWith('--')
&& (argv[i + 1] === undefined || argv[i + 1] === '' || argv[i + 1].startsWith('--'))) {
return { error: `${arg} needs a value` };
}
if (arg === '--file') { result.files ??= []; result.files.push(argv[++i]); continue; }
if (arg.startsWith('--')) { result[arg.slice(2).replaceAll('-', '')] = argv[++i]; continue; }
result._.push(arg);
Expand All @@ -47,6 +51,7 @@ function exitFor(status) {
function main(argv = process.argv.slice(2)) {
if (argv.includes('--help') || argv.includes('-h')) { process.stdout.write(`${usage()}\n`); return 0; }
const parsed = argsAfterCommand(argv);
if (parsed.error) { console.error(`${parsed.error}\n${usage()}`); return 2; }
let result;
if (parsed.command === 'validate') {
if (!parsed.root || !parsed.package || !parsed.registry || !parsed.roles || !parsed.pointers) {
Expand Down
14 changes: 14 additions & 0 deletions research-kit/test/path-authority-validator.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ test('path-authority validator rejects out-of-root paths, symlink claims, host m
assert(result.errors.some((error) => error.code === 'SNAP-ORIGIN-UNPROVEN'), JSON.stringify(result.errors));
});

test('path-authority names a missing value before validating any input', () => {
for (const flag of ['--file', '--schema', '--root']) {
for (const tail of [[], ['--json']]) {
const args = ['research-kit/bin/path-authority.mjs', 'validate', flag, ...tail];
const run = spawnSync(process.execPath, args, { encoding: 'utf8' });
const output = `${run.stdout}${run.stderr}`;
assertEqual(run.status, 2, `${args.join(' ')}: ${output}`);
assert(run.stderr.includes(`${flag} needs a value`), output);
assert(run.stderr.includes('Usage:'), output);
assert(!/TypeError|paths\[0\]|ERR_INVALID_ARG_TYPE/.test(output), output);
}
}
});

test('path-authority CLI conform and validate are offline, deterministic, and read-only', async () => {
const root = tempRoot();
try {
Expand Down
17 changes: 17 additions & 0 deletions research-kit/test/release-validator.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,23 @@ test('researcher-release validate emits the contract status and exit code withou
}
});

test('researcher-release names a missing value before resolving paths', () => {
for (const flag of ['--file', '--schema', '--root']) {
for (const tail of [[], ['--json']]) {
const args = [
'research-kit/bin/researcher-release.mjs', 'conform', '--schema', 'missing-schema.json',
'--file', 'missing-record.json', flag, ...tail,
];
const run = spawnSync(process.execPath, args, { encoding: 'utf8' });
const output = `${run.stdout}${run.stderr}`;
assertEqual(run.status, 2, `${args.join(' ')}: ${output}`);
assert(run.stderr.includes(`${flag} needs a value`), output);
assert(run.stderr.includes('Usage:'), output);
assert(!/TypeError|paths\[0\]|ERR_INVALID_ARG_TYPE/.test(output), output);
}
}
});

test('researcher-release conform validates explicit files and maps schema failure to exit 1', async () => {
const root = tempRoot();
try {
Expand Down
Loading