doc-detective-common 3.6.0-dev.2 → 3.6.1-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.c8rc.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "all": true,
3
+ "include": ["src/**/*.js"],
4
+ "exclude": ["src/schemas/dereferenceSchemas.js"],
5
+ "reporter": ["text", "lcov", "json", "json-summary"],
6
+ "report-dir": "coverage",
7
+ "check-coverage": false
8
+ }
@@ -0,0 +1,161 @@
1
+ # TDD and Coverage Skill
2
+
3
+ **Type:** Rigid (follow exactly)
4
+
5
+ ## When to Use
6
+
7
+ Use this skill when:
8
+ - Creating new functionality
9
+ - Modifying existing code
10
+ - Fixing bugs
11
+ - Refactoring
12
+
13
+ ## Mandatory Process
14
+
15
+ ### 1. Test First (TDD)
16
+
17
+ Before writing or modifying any implementation code:
18
+
19
+ 1. **Write the test(s)** that describe the expected behavior
20
+ 2. **Run the test** - it should FAIL (red)
21
+ 3. **Write the implementation** to make the test pass
22
+ 4. **Run the test** - it should PASS (green)
23
+ 5. **Refactor** if needed, keeping tests passing
24
+
25
+ ### 2. Coverage Verification
26
+
27
+ After any code change:
28
+
29
+ ```bash
30
+ # Run tests with coverage
31
+ npm run test:coverage
32
+
33
+ # Verify coverage hasn't decreased
34
+ npm run test:coverage:ratchet
35
+ ```
36
+
37
+ **Coverage must not decrease.** If ratchet check fails:
38
+ 1. Add tests for uncovered code
39
+ 2. Re-run coverage until ratchet passes
40
+
41
+ ### 3. Coverage Thresholds
42
+
43
+ Current thresholds are in `coverage-thresholds.json`. These values must only increase:
44
+
45
+ | Metric | Threshold |
46
+ |--------|-----------|
47
+ | Lines | 100% |
48
+ | Statements | 100% |
49
+ | Functions | 100% |
50
+ | Branches | 100% |
51
+
52
+ ### 4. Test Location
53
+
54
+ | Code | Test File |
55
+ |------|-----------|
56
+ | `src/validate.js` | `test/validate.test.js` |
57
+ | `src/resolvePaths.js` | `test/resolvePaths.test.js` |
58
+ | `src/files.js` | `test/files.test.js` |
59
+ | Schema validation | `test/schema.test.js` |
60
+
61
+ ### 5. Test Structure Pattern
62
+
63
+ ```javascript
64
+ const sinon = require("sinon");
65
+
66
+ (async () => {
67
+ const { expect } = await import("chai");
68
+ const { functionUnderTest } = require("../src/module");
69
+
70
+ describe("functionUnderTest", function () {
71
+ describe("input validation", function () {
72
+ it("should throw error when required param missing", function () {
73
+ expect(() => functionUnderTest()).to.throw();
74
+ });
75
+ });
76
+
77
+ describe("happy path", function () {
78
+ it("should return expected result for valid input", function () {
79
+ const result = functionUnderTest({ validInput: true });
80
+ expect(result).to.deep.equal(expectedOutput);
81
+ });
82
+ });
83
+
84
+ describe("edge cases", function () {
85
+ it("should handle boundary condition", function () {
86
+ // test edge case
87
+ });
88
+ });
89
+ });
90
+ })();
91
+ ```
92
+
93
+ ### 6. Checklist
94
+
95
+ Before completing any code change:
96
+
97
+ - [ ] Tests written BEFORE implementation (or for existing code: tests added)
98
+ - [ ] All tests pass (`npm test`)
99
+ - [ ] Coverage hasn't decreased (`npm run test:coverage:ratchet`)
100
+ - [ ] New code has corresponding test coverage
101
+ - [ ] Error paths are tested (not just happy paths)
102
+
103
+ ## Commands Reference
104
+
105
+ ```bash
106
+ # Run all tests
107
+ npm test
108
+
109
+ # Run tests with coverage report
110
+ npm run test:coverage
111
+
112
+ # Run coverage ratchet check
113
+ npm run test:coverage:ratchet
114
+
115
+ # Generate HTML coverage report
116
+ npm run test:coverage:html
117
+ ```
118
+
119
+ ## Common Patterns
120
+
121
+ ### Testing async functions
122
+
123
+ ```javascript
124
+ it("should handle async operation", async function () {
125
+ const result = await asyncFunction();
126
+ expect(result).to.exist;
127
+ });
128
+ ```
129
+
130
+ ### Mocking with Sinon
131
+
132
+ ```javascript
133
+ const stub = sinon.stub(fs, "readFileSync").returns("mock content");
134
+ try {
135
+ const result = functionUnderTest();
136
+ expect(result).to.equal("expected");
137
+ } finally {
138
+ stub.restore();
139
+ }
140
+ ```
141
+
142
+ ### Testing error handling
143
+
144
+ ```javascript
145
+ it("should throw on invalid input", function () {
146
+ expect(() => functionUnderTest(null)).to.throw(/error message/);
147
+ });
148
+ ```
149
+
150
+ ### Testing transformations
151
+
152
+ ```javascript
153
+ it("should transform v2 object to v3", function () {
154
+ const result = transformToSchemaKey({
155
+ currentSchema: "schema_v2",
156
+ targetSchema: "schema_v3",
157
+ object: v2Object,
158
+ });
159
+ expect(result.newProperty).to.equal(expectedValue);
160
+ });
161
+ ```
package/AGENTS.md CHANGED
@@ -106,6 +106,8 @@ When creating new schema version (e.g., v4):
106
106
  **Test structure (Mocha + Chai):**
107
107
  - `test/schema.test.js`: Validates all schema examples (auto-generated from schemas)
108
108
  - `test/files.test.js`: Unit tests for `readFile()` with Sinon stubs
109
+ - `test/validate.test.js`: Tests for `validate()` and `transformToSchemaKey()`
110
+ - `test/resolvePaths.test.js`: Tests for path resolution
109
111
 
110
112
  **Run tests:** `npm test` (or `mocha`)
111
113
 
@@ -115,6 +117,51 @@ const result = validate({ schemaKey: "step_v3", object: example });
115
117
  assert.ok(result.valid, `Validation failed: ${result.errors}`);
116
118
  ```
117
119
 
120
+ ### Testing Requirements (CRITICAL)
121
+
122
+ **TDD is mandatory for this project.** All code changes must follow test-driven development:
123
+
124
+ 1. **Write tests first** - before any implementation
125
+ 2. **Run tests** - verify they fail (red)
126
+ 3. **Write implementation** - make tests pass
127
+ 4. **Run tests** - verify they pass (green)
128
+ 5. **Check coverage** - must not decrease
129
+
130
+ **Coverage enforcement:**
131
+
132
+ ```bash
133
+ # Run tests with coverage
134
+ npm run test:coverage
135
+
136
+ # Verify coverage baseline (CI enforces this)
137
+ npm run test:coverage:ratchet
138
+
139
+ # Generate HTML report for detailed analysis
140
+ npm run test:coverage:html
141
+ ```
142
+
143
+ **Current coverage thresholds (enforced by CI):**
144
+
145
+ | Metric | Threshold |
146
+ |--------|-----------|
147
+ | Lines | 100% |
148
+ | Statements | 100% |
149
+ | Functions | 100% |
150
+ | Branches | 100% |
151
+
152
+ **Coverage ratchet:** Thresholds in `coverage-thresholds.json` can only increase. CI fails if coverage decreases.
153
+
154
+ **Test file mapping:**
155
+
156
+ | Source | Test File |
157
+ |--------|-----------|
158
+ | `src/validate.js` | `test/validate.test.js` |
159
+ | `src/resolvePaths.js` | `test/resolvePaths.test.js` |
160
+ | `src/files.js` | `test/files.test.js` |
161
+ | Schema examples | `test/schema.test.js` |
162
+
163
+ **AI Tooling:** See `.claude/skills/tdd-coverage/SKILL.md` for detailed TDD workflow.
164
+
118
165
  ### Version Management & CI/CD Workflows
119
166
 
120
167
  #### Auto Dev Release (`.github/workflows/auto-dev-release.yml`)
package/CLAUDE.md ADDED
@@ -0,0 +1,38 @@
1
+ # Claude Code Configuration
2
+
3
+ This file is a pointer for Claude Code and similar AI assistants.
4
+
5
+ ## Primary Documentation
6
+
7
+ See **[AGENTS.md](./AGENTS.md)** for complete project guidelines, architecture, and development workflows.
8
+
9
+ ## Quick Reference
10
+
11
+ ### Testing (CRITICAL)
12
+
13
+ **All code changes require TDD:**
14
+ 1. Write tests first
15
+ 2. Verify tests fail
16
+ 3. Write implementation
17
+ 4. Verify tests pass
18
+ 5. Check coverage: `npm run test:coverage:ratchet`
19
+
20
+ **Coverage must never decrease.**
21
+
22
+ ### Available Commands
23
+
24
+ ```bash
25
+ npm test # Run tests
26
+ npm run test:coverage # Tests + coverage report
27
+ npm run test:coverage:ratchet # Verify coverage baseline
28
+ npm run build # Build schemas
29
+ ```
30
+
31
+ ### Key Files
32
+
33
+ | Purpose | Location |
34
+ |---------|----------|
35
+ | Project guidelines | `AGENTS.md` |
36
+ | TDD/Coverage skill | `.claude/skills/tdd-coverage/SKILL.md` |
37
+ | Coverage config | `.c8rc.json` |
38
+ | Coverage baseline | `coverage-thresholds.json` |
@@ -0,0 +1,8 @@
1
+ {
2
+ "description": "Coverage baseline thresholds. These values should only increase, never decrease.",
3
+ "lastUpdated": "2026-01-07",
4
+ "lines": 100,
5
+ "statements": 100,
6
+ "functions": 100,
7
+ "branches": 100
8
+ }
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "doc-detective-common",
3
- "version": "3.6.0-dev.2",
3
+ "version": "3.6.1-dev.1",
4
4
  "description": "Shared components for Doc Detective projects.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
7
7
  "dereferenceSchemas": "node ./src/schemas/dereferenceSchemas.js",
8
8
  "build": "npm run dereferenceSchemas",
9
9
  "postbuild": "npm run test",
10
- "test": "mocha"
10
+ "test": "mocha",
11
+ "test:coverage": "c8 mocha",
12
+ "test:coverage:html": "c8 --reporter=html mocha",
13
+ "test:coverage:check": "c8 check-coverage",
14
+ "test:coverage:ratchet": "node scripts/check-coverage-ratchet.js"
11
15
  },
12
16
  "repository": {
13
17
  "type": "git",
@@ -20,6 +24,7 @@
20
24
  },
21
25
  "homepage": "https://github.com/doc-detective/doc-detective-common#readme",
22
26
  "devDependencies": {
27
+ "c8": "^10.1.3",
23
28
  "chai": "^6.2.2",
24
29
  "mocha": "^11.7.5",
25
30
  "sinon": "^21.0.1"
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Coverage Ratchet Script
5
+ *
6
+ * Compares current coverage against baseline thresholds.
7
+ * Fails if any metric has decreased.
8
+ *
9
+ * Usage: node scripts/check-coverage-ratchet.js
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const THRESHOLDS_FILE = path.join(__dirname, '..', 'coverage-thresholds.json');
16
+ const COVERAGE_SUMMARY_FILE = path.join(__dirname, '..', 'coverage', 'coverage-summary.json');
17
+
18
+ /**
19
+ * Load and parse JSON from the given path, exiting the process with code 1 if the file is missing or cannot be parsed.
20
+ * @param {string} filePath - Filesystem path to the JSON file.
21
+ * @param {string} description - Human-readable name for the file used in error messages.
22
+ * @returns {Object} The parsed JSON object.
23
+ */
24
+ function loadJSON(filePath, description) {
25
+ if (!fs.existsSync(filePath)) {
26
+ console.error(`Error: ${description} not found at ${filePath}`);
27
+ console.error(`Run 'npm run test:coverage' first to generate coverage data.`);
28
+ process.exit(1);
29
+ }
30
+
31
+ try {
32
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
33
+ } catch (error) {
34
+ console.error(`Error parsing ${description}: ${error.message}`);
35
+ process.exit(1);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Compares current test coverage against the stored baseline thresholds and enforces the coverage ratchet.
41
+ *
42
+ * Loads baseline thresholds and the current coverage summary, prints a per-metric table of baseline vs current values and statuses, and enforces policy:
43
+ * - Exits with code 1 if any metric has decreased relative to the baseline.
44
+ * - If one or more metrics have improved, prints suggested threshold updates.
45
+ * - Exits with code 0 when all metrics meet or exceed their baselines.
46
+ */
47
+ function main() {
48
+ // Load baseline thresholds
49
+ const thresholds = loadJSON(THRESHOLDS_FILE, 'Coverage thresholds file');
50
+
51
+ // Load current coverage
52
+ const coverageSummary = loadJSON(COVERAGE_SUMMARY_FILE, 'Coverage summary');
53
+
54
+ const current = coverageSummary.total;
55
+ const metrics = ['lines', 'statements', 'functions', 'branches'];
56
+
57
+ let failed = false;
58
+ const results = [];
59
+
60
+ console.log('\n=== Coverage Ratchet Check ===\n');
61
+ console.log('Metric | Baseline | Current | Status');
62
+ console.log('------------|----------|----------|--------');
63
+
64
+ for (const metric of metrics) {
65
+ const baseline = thresholds[metric];
66
+ const currentValue = current[metric].pct;
67
+ const diff = (currentValue - baseline).toFixed(2);
68
+
69
+ let status;
70
+ if (currentValue < baseline) {
71
+ status = `FAIL (${diff}%)`;
72
+ failed = true;
73
+ } else if (currentValue > baseline) {
74
+ status = `PASS (+${diff}%)`;
75
+ } else {
76
+ status = 'PASS';
77
+ }
78
+
79
+ const baselineStr = `${baseline.toFixed(2)}%`.padEnd(8);
80
+ const currentStr = `${currentValue.toFixed(2)}%`.padEnd(8);
81
+ const metricStr = metric.padEnd(11);
82
+
83
+ console.log(`${metricStr} | ${baselineStr} | ${currentStr} | ${status}`);
84
+
85
+ results.push({
86
+ metric,
87
+ baseline,
88
+ current: currentValue,
89
+ passed: currentValue >= baseline
90
+ });
91
+ }
92
+
93
+ console.log('');
94
+
95
+ if (failed) {
96
+ console.error('Coverage ratchet check FAILED!');
97
+ console.error('Coverage has decreased from the baseline.');
98
+ console.error('Please add tests to restore coverage before committing.');
99
+ process.exit(1);
100
+ }
101
+
102
+ // Check if we can bump thresholds
103
+ const canBump = results.filter(r => r.current > r.baseline);
104
+ if (canBump.length > 0) {
105
+ console.log('Coverage has improved! Consider updating thresholds:');
106
+ console.log('');
107
+ for (const r of canBump) {
108
+ console.log(` "${r.metric}": ${r.current.toFixed(2)}`);
109
+ }
110
+ console.log('');
111
+ console.log(`Update ${THRESHOLDS_FILE} to lock in the new baseline.`);
112
+ }
113
+
114
+ console.log('Coverage ratchet check PASSED!');
115
+ process.exit(0);
116
+ }
117
+
118
+ main();
@@ -5,19 +5,18 @@ const { validate } = require("./validate");
5
5
  exports.resolvePaths = resolvePaths;
6
6
 
7
7
  /**
8
- * Recursively resolves all relative path properties in a configuration or specification object to absolute paths.
8
+ * Convert recognized relative path properties in a config or spec object to absolute paths.
9
9
  *
10
- * Traverses the provided object, converting all recognized path-related properties to absolute paths using the given configuration and reference file path. Supports nested objects and distinguishes between config and spec objects based on schema validation. Throws an error if the object is not a valid config or spec, or if the object type is missing for nested objects.
10
+ * Traverses the provided object (recursing into nested objects and arrays), resolving fields that represent filesystem paths according to the provided config.relativePathBase and reference filePath. On top-level calls the function infers whether the object is a config or spec via schema validation; for nested calls objectType must be provided.
11
11
  *
12
- * @async
13
12
  * @param {Object} options - Options for path resolution.
14
- * @param {Object} options.config - Configuration object containing settings such as `relativePathBase`.
13
+ * @param {Object} options.config - Configuration containing settings such as `relativePathBase`.
15
14
  * @param {Object} options.object - The config or spec object whose path properties will be resolved.
16
- * @param {string} options.filePath - Reference file path used for resolving relative paths.
17
- * @param {boolean} [options.nested=false] - Indicates if this is a recursive call for a nested object.
18
- * @param {string} [options.objectType] - Specifies the object type ('config' or 'spec'); required for nested objects.
19
- * @returns {Promise<Object>} The object with all applicable path properties resolved to absolute paths.
20
- * @throws {Error} If the object is neither a valid config nor spec, or if `objectType` is missing for nested objects.
15
+ * @param {string} options.filePath - Reference file or directory used to resolve relative paths.
16
+ * @param {boolean} [options.nested=false] - True when invoked recursively for nested objects.
17
+ * @param {string} [options.objectType] - 'config' or 'spec'; required for nested invocations to select which properties to resolve.
18
+ * @returns {Object} The same object with applicable path properties converted to absolute paths.
19
+ * @throws {Error} If the top-level object matches neither config nor spec schema, or if `objectType` is missing for nested calls.
21
20
  */
22
21
  async function resolvePaths({
23
22
  config,
@@ -223,6 +222,7 @@ async function resolvePaths({
223
222
  }
224
223
 
225
224
  // If called directly, resolve paths in the provided object
225
+ /* c8 ignore start */
226
226
  if (require.main === module) {
227
227
  (async () => {
228
228
  // Example usage
@@ -250,3 +250,4 @@ if (require.main === module) {
250
250
  console.log(JSON.stringify(object, null, 2));
251
251
  })();
252
252
  }
253
+ /* c8 ignore stop */
package/src/validate.js CHANGED
@@ -148,6 +148,7 @@ function validate({ schemaKey, object, addDefaults = true }) {
148
148
  if (result.valid) {
149
149
  validationObject = transformedObject;
150
150
  object = transformedObject;
151
+ /* c8 ignore start - Defensive: transformToSchemaKey validates internally, so this is unreachable */
151
152
  } else if (check.errors) {
152
153
  const errors = check.errors.map(
153
154
  (error) =>
@@ -158,6 +159,7 @@ function validate({ schemaKey, object, addDefaults = true }) {
158
159
  result.errors = errors.join(", ");
159
160
  return result;
160
161
  }
162
+ /* c8 ignore stop */
161
163
  }
162
164
  }
163
165
  if (addDefaults) {
@@ -170,18 +172,14 @@ function validate({ schemaKey, object, addDefaults = true }) {
170
172
  }
171
173
 
172
174
  /**
173
- * Transforms an object from one JSON schema version to another, supporting multiple schema types and nested conversions.
175
+ * Transform an object from one schema key to another and return a validated instance of the target schema.
174
176
  *
175
- * @param {Object} params
176
- * @param {string} params.currentSchema - The schema key of the object's current version.
177
- * @param {string} params.targetSchema - The schema key to which the object should be transformed.
178
- * @param {Object} params.object - The object to transform.
179
- * @returns {Object} The transformed object, validated against the target schema.
180
- *
181
- * @throws {Error} If transformation between the specified schemas is not supported, or if the transformed object fails validation.
182
- *
183
- * @remark
184
- * Supports deep and recursive transformations for complex schema types, including steps, configs, contexts, OpenAPI integrations, specs, and tests. Throws if the schemas are incompatible or if the resulting object does not conform to the target schema.
177
+ * @param {Object} params - Function parameters.
178
+ * @param {string} params.currentSchema - Schema key representing the object's current version.
179
+ * @param {string} params.targetSchema - Schema key to transform the object into.
180
+ * @param {Object} params.object - The source object to transform.
181
+ * @returns {Object} The transformed object conforming to the target schema.
182
+ * @throws {Error} If transformation between the specified schemas is not supported or if the transformed object fails validation.
185
183
  */
186
184
  function transformToSchemaKey({
187
185
  currentSchema = "",
@@ -445,6 +443,8 @@ function transformToSchemaKey({
445
443
  schemaKey: "config_v3",
446
444
  object: transformedObject,
447
445
  });
446
+ // Defensive: transformation always produces valid config_v3, unreachable
447
+ /* c8 ignore next 3 */
448
448
  if (!result.valid) {
449
449
  throw new Error(`Invalid object: ${result.errors}`);
450
450
  }
@@ -528,6 +528,8 @@ function transformToSchemaKey({
528
528
  schemaKey: "spec_v3",
529
529
  object: transformedObject,
530
530
  });
531
+ // Defensive: nested transforms validate; this is unreachable
532
+ /* c8 ignore next 3 */
531
533
  if (!result.valid) {
532
534
  throw new Error(`Invalid object: ${result.errors}`);
533
535
  }
@@ -570,18 +572,25 @@ function transformToSchemaKey({
570
572
  schemaKey: "test_v3",
571
573
  object: transformedObject,
572
574
  });
575
+ // Defensive: nested transforms validate; this is unreachable
576
+ /* c8 ignore next 3 */
573
577
  if (!result.valid) {
574
578
  throw new Error(`Invalid object: ${result.errors}`);
575
579
  }
576
580
  return result.object;
577
581
  }
582
+ /* c8 ignore next - Dead code: incompatible schemas throw at line 197-200 */
578
583
  return null;
579
584
  }
580
585
 
581
586
  // If called directly, validate an example object
587
+ /* c8 ignore start */
582
588
  if (require.main === module) {
583
- const example = {path: "/User/manny/projects/doc-detective/static/images/image.png"};
589
+ const example = {
590
+ path: "/User/manny/projects/doc-detective/static/images/image.png",
591
+ };
584
592
 
585
593
  const result = validate({ schemaKey: "screenshot_v3", object: example });
586
594
  console.log(JSON.stringify(result, null, 2));
587
595
  }
596
+ /* c8 ignore stop */