@dogfood-lab/ingest 1.2.2 → 1.3.0

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.
@@ -1,83 +1,77 @@
1
- /**
2
- * Persisted-record schema validator.
3
- *
4
- * Enforces dogfood-record.schema.json at write time. The submission validator
5
- * in @dogfood-lab/verify covers the inbound payload; this is the symmetric
6
- * gate on the outbound payload — the central verifier assembles the record
7
- * and the persist layer must not write anything that violates the contract.
8
- *
9
- * Mirrors the Ajv idiom in @dogfood-lab/verify/validators/schema.js same
10
- * Ajv2020 + ajv-formats setup, lazy compile, cached compiled validator.
11
- * Kept in this package (not extracted to a shared util) because the two
12
- * call sites have different error shapes and lifecycles: submission validation
13
- * returns { valid, errors } so the verifier can build a rejection record;
14
- * record validation throws because a malformed record reaching the write
15
- * path is a programming error, not user input.
16
- */
17
-
18
- import Ajv2020 from 'ajv/dist/2020.js';
19
- import addFormats from 'ajv-formats';
20
- import { readFileSync } from 'node:fs';
21
- import { createRequire } from 'node:module';
22
-
23
- const require = createRequire(import.meta.url);
24
- const SCHEMA_PATH = require.resolve('@dogfood-lab/schemas/json/dogfood-record.schema.json');
25
-
26
- let _validator = null;
27
- let _loadError = null;
28
-
29
- function getValidator() {
30
- if (_validator) return _validator;
31
- if (_loadError) throw _loadError;
32
-
33
- try {
34
- const ajv = new Ajv2020({ allErrors: true, strict: false });
35
- addFormats(ajv);
36
- const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf-8'));
37
- _validator = ajv.compile(schema);
38
- return _validator;
39
- } catch (e) {
40
- _loadError = new Error(`record schema load failed: ${e.message}`);
41
- throw _loadError;
42
- }
43
- }
44
-
45
- /**
46
- * Structured error thrown when a persisted record violates the schema.
47
- * Caller code can `instanceof RecordValidationError` to distinguish from
48
- * IO/path errors.
49
- */
50
- export class RecordValidationError extends Error {
51
- constructor(errors) {
52
- const summary = errors
53
- .map(e => `${e.path || '/'} ${e.message}`)
54
- .join('; ');
55
- super(`persisted record failed schema validation: ${summary}`);
56
- this.name = 'RecordValidationError';
57
- this.code = 'RECORD_SCHEMA_INVALID';
58
- this.errors = errors;
59
- }
60
- }
61
-
62
- /**
63
- * Validate a persisted record against dogfood-record.schema.json.
64
- * Throws RecordValidationError on failure; returns the record on success
65
- * so callers can chain (`writeFile(validateRecord(r))`).
66
- *
67
- * @param {object} record
68
- * @returns {object} the same record reference, unchanged
69
- * @throws {RecordValidationError}
70
- */
71
- export function validateRecord(record) {
72
- const validate = getValidator();
73
- const valid = validate(record);
74
- if (valid) return record;
75
-
76
- const errors = (validate.errors || []).map(err => ({
77
- path: err.instancePath || '/',
78
- keyword: err.keyword,
79
- message: err.message,
80
- params: err.params
81
- }));
82
- throw new RecordValidationError(errors);
83
- }
1
+ /**
2
+ * Persisted-record schema validator.
3
+ *
4
+ * Enforces dogfood-record.schema.json at write time. The submission validator
5
+ * in @dogfood-lab/verify covers the inbound payload; this is the symmetric
6
+ * gate on the outbound payload — the central verifier assembles the record
7
+ * and the persist layer must not write anything that violates the contract.
8
+ *
9
+ * H3 hop 4 (LAST the C1-sealing hop): delegates to the canonical
10
+ * {@link validatePayload} from `@dogfood-lab/schemas`. Pre-H3 this module
11
+ * compiled its own Ajv2020 + ajv-formats instance for the record schema,
12
+ * which was the SAME schema the verifier-side path (now also canonical)
13
+ * compiled in a SECOND instance the structural root of the C1 two-Ajv
14
+ * gap. After H3, ingest and verify share the single cached validator the
15
+ * canonical seam holds for `dogfood-record.schema.json`.
16
+ *
17
+ * Contract preserved:
18
+ * - Throws {@link RecordValidationError} on validation failure (NOT a
19
+ * return value — the persist layer treats a malformed record as a
20
+ * programming error, not user input).
21
+ * - `.code === 'RECORD_SCHEMA_INVALID'` is the structural error-codes
22
+ * gate pin (just hardened in A2.1 FX2 — see scripts/doc-drift-patterns.json
23
+ * error-codes check); the migration keeps it intact.
24
+ * - Each `errors[]` entry carries `{ path, keyword, message, params }` —
25
+ * the keyword pin (packages/ingest/ingest.test.js:208-220) asserts
26
+ * `typeof e.keyword === 'string'` for every error. The H3 keyword
27
+ * extension at the canonical seam (validate.ts ValidationError) is
28
+ * what makes this contract preservable without ingest holding its
29
+ * own Ajv instance.
30
+ */
31
+
32
+ import { validatePayload } from '@dogfood-lab/schemas';
33
+
34
+ /**
35
+ * Structured error thrown when a persisted record violates the schema.
36
+ * Caller code can `instanceof RecordValidationError` to distinguish from
37
+ * IO/path errors.
38
+ */
39
+ export class RecordValidationError extends Error {
40
+ constructor(errors) {
41
+ const summary = errors
42
+ .map(e => `${e.path || '/'} ${e.message}`)
43
+ .join('; ');
44
+ super(`persisted record failed schema validation: ${summary}`);
45
+ this.name = 'RecordValidationError';
46
+ this.code = 'RECORD_SCHEMA_INVALID';
47
+ this.errors = errors;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Validate a persisted record against dogfood-record.schema.json.
53
+ * Throws RecordValidationError on failure; returns the record on success
54
+ * so callers can chain (`writeFile(validateRecord(r))`).
55
+ *
56
+ * @param {object} record
57
+ * @returns {object} the same record reference, unchanged
58
+ * @throws {RecordValidationError}
59
+ */
60
+ export function validateRecord(record) {
61
+ const result = validatePayload('record', record);
62
+ if (result.valid) return record;
63
+
64
+ // Project canonical ValidationError ingest's historical error shape:
65
+ // pre-H3 entries had `{ path, keyword, message, params }`. Canonical
66
+ // ships the same fields (keyword added in H3 hop 0). Re-construct
67
+ // explicitly so any future change to the canonical shape (extra
68
+ // fields, renames) trips an explicit migration here rather than
69
+ // silently widening RecordValidationError.errors[].
70
+ const errors = result.errors.map(err => ({
71
+ path: err.path,
72
+ keyword: err.keyword,
73
+ message: err.message,
74
+ params: err.params,
75
+ }));
76
+ throw new RecordValidationError(errors);
77
+ }