NORTHSET
Proof-of-Pass Receipt — M-012
CONTRIBUTOR SELF-RUN — NOT MAINTAINER VERIFICATION
Project
Work
Consideration: detect `doc-kit.config.mjs` automatically · PR #901
Verification execution
runtime: northset-oss executor v0
human operator: aeziz
Code
- base
11d012a9956fc778a024a37488f0783e4b1cf90f- recorded patch commit
a0bdd2de6a81b60e2f8a607a40d39b4fa0b22751
declared metadata; not execution-bound- patch diff SHA-256
sha256:524fa5413c71090d9b4d4ad8dfd20f877f163551943875f83db2fbde9903beb3
bound to executed patch bytes
Environment
- image reference
- node:22-bookworm
- repository digest
node@sha256:a25c9934ff6382cd4f08b6bc26c82bf4ea69b1e6f8dabfb2ead457374127c365- network
- phaseA:bridge,phaseB:none
Declared checks
Execution summary
1/1 declared command returned exit 0 in the recorded environment
node --test --experimental-test-module-mocks src/utils/configuration/__tests__/index.test.mjsexit 0 · 0.7s
unclassified executor time (derived residual) 36s
run wall (derived from recorded timestamps) 36.7s
PASS — 1/1 declared command
Every command listed returned exit 0 in the declared environment. Only the listed commands are in scope. Unlisted test, lint, typecheck, build, coverage, compiler, full-suite, and CI gates are not implied or recorded.
Public scope interpretation
The declared network-off check runs only the configuration unit test (src/utils/configuration/__tests__/index.test.mjs) on node:22 after a disclosed online npm ci. It does not run the full test suite, eslint, prettier, or the TypeScript/JSDoc type-check gates that upstream CI also runs.
Record details
- payment
- none · not merge-contingent
- redactions
- none recorded
- Bundle contents digest
sha256:2c6b24a0e00782c386b6faf42945791584006b7e510c63b337acec7d621ef891- Signed asset SHA-256
sha256:653a46b74e428acb75738ae1e7b8a1b2b66cb36933087927538a971032561cdf- Signed provenance recorded
- verified 2026-07-14T14:21:48Z
NOT INCLUDED
- Does not prove code quality
- Does not prove security
- Contributor self-run record of Northset's own contribution; not the maintainer's verification.
- The declared network-off check runs only the configuration unit test (src/utils/configuration/__tests__/index.test.mjs) on node:22 after a disclosed online npm ci. It does not run the full test suite, eslint, prettier, or the TypeScript/JSDoc type-check gates that upstream CI also runs.
Signed bundle
Verify this receipt
gh attestation verify run-record-M-012.tar.gz --repo northset-oss/verification-pilot --signer-workflow northset-oss/verification-pilot/.github/workflows/attest-bundle.ymlAttestation confirms that Northset's signing workflow produced this exact bundle. The signer does not witness the recorded run, and verification does not turn it into maintainer verification.
QR → receipt pageCommitted patch.diff
diff --git a/.changeset/quiet-dingos-detect.md b/.changeset/quiet-dingos-detect.md
new file mode 100644
index 0000000..fb55278
--- /dev/null
+++ b/.changeset/quiet-dingos-detect.md
@@ -0,0 +1,5 @@
+---
+'@node-core/doc-kit': patch
+---
+
+Automatically load `doc-kit.config.mjs` from the current working directory.
diff --git a/src/utils/configuration/__tests__/index.test.mjs b/src/utils/configuration/__tests__/index.test.mjs
index ac7bf50..8ce318f 100644
--- a/src/utils/configuration/__tests__/index.test.mjs
+++ b/src/utils/configuration/__tests__/index.test.mjs
@@ -1,10 +1,13 @@
import assert from 'node:assert';
+import * as nodeFs from 'node:fs';
+import { join } from 'node:path';
import { describe, it, mock, beforeEach } from 'node:test';
// Mock dependencies
const mockParseChangelog = mock.fn(async changelog => [changelog]);
const mockParseIndex = mock.fn(async index => [index]);
const mockImportFromURL = mock.fn(async () => ({}));
+const mockExistsSync = mock.fn(() => false);
const createMockConfig = (overrides = {}) => ({
global: {},
@@ -31,6 +34,9 @@ mock.module('../../../parsers/markdown.mjs', {
mock.module('../../loaders.mjs', {
namedExports: { importFromURL: mockImportFromURL },
});
+mock.module('node:fs', {
+ namedExports: { ...nodeFs, existsSync: mockExistsSync },
+});
const {
assertRunnableOptions,
@@ -43,9 +49,12 @@ const {
// Helper to reset all mocks
const resetAllMocks = () => {
- [mockParseChangelog, mockParseIndex, mockImportFromURL].forEach(m =>
- m.mock.resetCalls()
- );
+ [
+ mockParseChangelog,
+ mockParseIndex,
+ mockImportFromURL,
+ mockExistsSync,
+ ].forEach(m => m.mock.resetCalls());
};
// Helper to count specific function calls
@@ -149,6 +158,47 @@ describe('config.mjs', () => {
});
describe('createRunConfiguration', () => {
+ it('should auto-detect a config file in the current directory', async () => {
+ const defaultConfigFile = join(process.cwd(), 'doc-kit.config.mjs');
+ const mockConfig = createMockConfig({
+ global: { input: 'auto-detected-src/' },
+ });
+ mockExistsSync.mock.mockImplementationOnce(() => true);
+ mockImportFromURL.mock.mockImplementationOnce(async () => mockConfig);
+
+ const config = await createRunConfiguration({});
+
+ assert.strictEqual(config.global.input, 'auto-detected-src/');
+ assert.strictEqual(mockExistsSync.mock.calls.length, 1);
+ assert.strictEqual(
+ mockExistsSync.mock.calls[0].arguments[0],
+ defaultConfigFile
+ );
+ assert.strictEqual(mockImportFromURL.mock.calls.length, 1);
+ assert.strictEqual(
+ mockImportFromURL.mock.calls[0].arguments[0],
+ defaultConfigFile
+ );
+ });
+
+ it('should prefer an explicit config file', async () => {
+ mockImportFromURL.mock.mockImplementationOnce(async () =>
+ createMockConfig({ global: { input: 'explicit-src/' } })
+ );
+
+ const config = await createRunConfiguration({
+ configFile: 'explicit-config.mjs',
+ });
+
+ assert.strictEqual(config.global.input, 'explicit-src/');
+ assert.strictEqual(mockExistsSync.mock.calls.length, 0);
+ assert.strictEqual(mockImportFromURL.mock.calls.length, 1);
+ assert.strictEqual(
+ mockImportFromURL.mock.calls[0].arguments[0],
+ 'explicit-config.mjs'
+ );
+ });
+
it('should merge config sources in correct order', async () => {
mockImportFromURL.mock.mockImplementationOnce(async () =>
createMockConfig({ global: { input: 'custom-src/' } })
@@ -204,7 +254,7 @@ describe('config.mjs', () => {
assert.strictEqual(config.chunkSize, 1);
});
- it('should work without config file', async () => {
+ it('should use an empty config when no config file is present', async () => {
const config = await createRunConfiguration({
version: '20.0.0',
threads: 4,
@@ -212,6 +262,11 @@ describe('config.mjs', () => {
assert.ok(config);
assert.strictEqual(config.threads, 4);
+ assert.strictEqual(mockExistsSync.mock.calls.length, 1);
+ assert.strictEqual(
+ mockExistsSync.mock.calls[0].arguments[0],
+ join(process.cwd(), 'doc-kit.config.mjs')
+ );
assert.strictEqual(mockImportFromURL.mock.calls.length, 0);
});
diff --git a/src/utils/configuration/index.mjs b/src/utils/configuration/index.mjs
index a31bc35..9f5536b 100644
--- a/src/utils/configuration/index.mjs
+++ b/src/utils/configuration/index.mjs
@@ -1,4 +1,6 @@
+import { existsSync } from 'node:fs';
import { cpus } from 'node:os';
+import { join } from 'node:path';
import { isMainThread } from 'node:worker_threads';
import { coerce } from 'semver';
@@ -123,7 +125,8 @@ export const assertRunnableOptions = config => {
};
/**
- * Creates a complete run configuration by merging config file, user options, and defaults.
+ * Creates a complete run configuration by merging an explicit or auto-detected
+ * config file, user options, and defaults.
* Processes and validates configuration values including version coercion, changelog parsing,
* and constraint enforcement for threads and chunk size.
*
@@ -131,7 +134,11 @@ export const assertRunnableOptions = config => {
* @returns {Promise<import('./types').Configuration>} The configuration
*/
export const createRunConfiguration = async options => {
- const config = await loadConfigFile(options.configFile);
+ const defaultConfigFile = join(process.cwd(), 'doc-kit.config.mjs');
+ const configFile =
+ options.configFile ??
+ (existsSync(defaultConfigFile) ? defaultConfigFile : undefined);
+ const config = await loadConfigFile(configFile);
config.target &&= enforceArray(config.target);
// Merge with defaults
Redacted stdout
=== cmd 1: node --test --experimental-test-module-mocks src/utils/configuration/__tests__/index.test.mjs ===
TAP version 13
# (node:16) ExperimentalWarning: Module mocking is an experimental feature and might change at any time
# (Use `node --trace-warnings ...` to show where the warning was created)
# Subtest: config.mjs
# Subtest: loadConfigFile
# Subtest: should load config from file path
ok 1 - should load config from file path
---
duration_ms: 0.750042
type: 'test'
...
# Subtest: should return empty object for falsy paths
ok 2 - should return empty object for falsy paths
---
duration_ms: 0.4375
type: 'test'
...
1..2
ok 1 - loadConfigFile
---
duration_ms: 1.599792
type: 'suite'
...
# Subtest: createConfigFromCLIOptions
# Subtest: should convert CLI options to config structure
ok 1 - should convert CLI options to config structure
---
duration_ms: 0.916875
type: 'test'
...
# Subtest: should handle empty options
ok 2 - should handle empty options
---
duration_ms: 1.205833
type: 'test'
...
1..2
ok 2 - createConfigFromCLIOptions
---
duration_ms: 2.334
type: 'suite'
...
# Subtest: assertRunnableOptions
# Subtest: should throw when target is missing
ok 1 - should throw when target is missing
---
duration_ms: 0.567875
type: 'test'
...
# Subtest: should throw when input is missing
ok 2 - should throw when input is missing
---
duration_ms: 0.137959
type: 'test'
...
# Subtest: should not throw when both target and input are provided
ok 3 - should not throw when both target and input are provided
---
duration_ms: 0.314917
type: 'test'
...
1..3
ok 3 - assertRunnableOptions
---
duration_ms: 1.220542
type: 'suite'
...
# Subtest: createRunConfiguration
# Subtest: should auto-detect a config file in the current directory
ok 1 - should auto-detect a config file in the current directory
---
duration_ms: 1.383042
type: 'test'
...
# Subtest: should prefer an explicit config file
ok 2 - should prefer an explicit config file
---
duration_ms: 0.285959
type: 'test'
...
# Subtest: should merge config sources in correct order
ok 3 - should merge config sources in correct order
---
duration_ms: 0.3115
type: 'test'
...
# Subtest: should transform string values only once
ok 4 - should transform string values only once
---
duration_ms: 0.305416
type: 'test'
...
# Subtest: should enforce minimum constraints
ok 5 - should enforce minimum constraints
---
duration_ms: 0.172875
type: 'test'
...
# Subtest: should use an empty config when no config file is present
ok 6 - should use an empty config when no config file is present
---
duration_ms: 0.25425
type: 'test'
...
# Subtest: should handle generator-specific overrides
ok 7 - should handle generator-specific overrides
---
duration_ms: 0.340833
type: 'test'
...
1..7
ok 4 - createRunConfiguration
---
duration_ms: 3.230959
type: 'suite'
...
# Subtest: setConfig and getConfig
# Subtest: should persist config across calls
ok 1 - should persist config across calls
---
duration_ms: 0.276209
type: 'test'
...
1..1
ok 5 - setConfig and getConfig
---
duration_ms: 0.339458
type: 'suite'
...
# Subtest: transformation optimization
# Subtest: should transform changelog parsing only for strings
ok 1 - should transform changelog parsing only for strings
---
duration_ms: 0.406833
type: 'test'
...
# Subtest: should transform index parsing only for strings
ok 2 - should transform index parsing only for strings
---
duration_ms: 0.22975
type: 'test'
...
1..2
ok 6 - transformation optimization
---
duration_ms: 0.725042
type: 'suite'
...
1..6
ok 1 - config.mjs
---
duration_ms: 10.071958
type: 'suite'
...
1..1
# tests 17
# suites 7
# pass 17
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 418.699959
Redacted stderr
=== cmd 1: node --test --experimental-test-module-mocks src/utils/configuration/__tests__/index.test.mjs ===