yarramate 1.15.0 → 1.15.2

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,5 +1,6 @@
1
1
  import Ajv2020Module from 'ajv/dist/2020.js';
2
2
  import { diagnosticOrder, loadSourceDocument, locateSourcePath, } from './source-document.js';
3
+ import { lazyValidator } from './schema-validation.js';
3
4
  import adapterMappingSchema from '../schema/yarramate-adapter-mapping.schema.json' with { type: 'json'
4
5
  };
5
6
  // `.default ?? module`, not a bare `.default`: NodeNext sees the raw CJS
@@ -8,10 +9,10 @@ import adapterMappingSchema from '../schema/yarramate-adapter-mapping.schema.jso
8
9
  // to keep track of (#252).
9
10
  const ajv2020Module = Ajv2020Module;
10
11
  const Ajv2020 = ajv2020Module.default ?? ajv2020Module;
11
- const validateSchema = new Ajv2020({ allErrors: true }).compile(adapterMappingSchema);
12
+ const validateSchema = lazyValidator(() => new Ajv2020({ allErrors: true }).compile(adapterMappingSchema));
12
13
  const mappingLocations = new WeakMap();
13
14
  export function loadAdapterMapping(source) {
14
- const loaded = loadSourceDocument(source, validateSchema, 'Adapter mapping');
15
+ const loaded = loadSourceDocument(source, validateSchema(), 'Adapter mapping');
15
16
  if (!loaded.ok)
16
17
  return loaded;
17
18
  const { value, yaml, lineCounter } = loaded.document;
@@ -22,6 +22,7 @@ export const posixDirectoryOf = (path) => {
22
22
  };
23
23
  import operationsSchema from '../schema/yarramate-operations.schema.json' with { type: 'json'
24
24
  };
25
+ import { lazyValidator } from './schema-validation.js';
25
26
  // `.default ?? module`, not a bare `.default`: NodeNext sees the raw CJS
26
27
  // `module.exports` and a bundler sees the unwrapped class, and this file is
27
28
  // reachable from a browser through `./apply-operations.js` (#252).
@@ -29,10 +30,12 @@ const ajv2020Module = Ajv2020Module;
29
30
  const Ajv2020 = ajv2020Module.default ?? ajv2020Module;
30
31
  // `discriminator` routes a batch entry to the single branch its `op` names, so
31
32
  // one malformed operation reports one fault instead of ten near-misses.
32
- const validateOperations = new Ajv2020({
33
+ // Keeps its own Ajv instance: `discriminator` changes how a schema compiles,
34
+ // so it cannot share one with the nine that do not set it.
35
+ const validateOperations = lazyValidator(() => new Ajv2020({
33
36
  allErrors: true,
34
37
  discriminator: true,
35
- }).compile(operationsSchema);
38
+ }).compile(operationsSchema));
36
39
  // Scalar fields replace; list fields append; `remove` retracts (ADR 0062).
37
40
  // An answer enriches what is there and may explicitly take back what it
38
41
  // asserted — it never silently shrinks anything.
@@ -310,7 +313,7 @@ export const applyOperations = (input) => {
310
313
  ok: false,
311
314
  diagnostics,
312
315
  });
313
- const loadedOperations = loadSourceDocument(operations, validateOperations, 'Operations');
316
+ const loadedOperations = loadSourceDocument(operations, validateOperations(), 'Operations');
314
317
  if (!loadedOperations.ok)
315
318
  return failed(loadedOperations.diagnostics);
316
319
  const operationList = loadedOperations.document.value.operations;
package/dist/compiler.js CHANGED
@@ -11,12 +11,13 @@ import patternSchema from '../schema/yarramate-pattern.schema.json' with { type:
11
11
  };
12
12
  import { ATTESTATION_PREDICATE_PREFIX, attestationClaimValue } from './graph-claims.js';
13
13
  import { shippedPolicyIdentity, shippedPolicySource, } from './shipped-profile.js';
14
+ import { lazyValidator } from './schema-validation.js';
14
15
  const coreProfile = 'yarramate/core@0.1';
15
16
  const ajv2020Module = Ajv2020Import;
16
17
  const Ajv2020 = ajv2020Module.default ?? ajv2020Module;
17
- const validateDocument = new Ajv2020({ allErrors: true }).compile(documentSchema);
18
- const validateProfile = new Ajv2020({ allErrors: true }).compile(profileSchema);
19
- const validatePattern = new Ajv2020({ allErrors: true }).compile(patternSchema);
18
+ const validateDocument = lazyValidator(() => new Ajv2020({ allErrors: true }).compile(documentSchema));
19
+ const validateProfile = lazyValidator(() => new Ajv2020({ allErrors: true }).compile(profileSchema));
20
+ const validatePattern = lazyValidator(() => new Ajv2020({ allErrors: true }).compile(patternSchema));
20
21
  const immutableMap = (entries) => {
21
22
  const backing = new Map(entries);
22
23
  const facade = {
@@ -160,12 +161,12 @@ const parseWorkspaceSource = (input) => {
160
161
  fresh,
161
162
  };
162
163
  }
163
- const valid = parseDiagnostics.length === 0 && validateDocument(value);
164
+ const valid = parseDiagnostics.length === 0 && validateDocument()(value);
164
165
  const schemaDiagnostics = parseDiagnostics.length > 0
165
166
  ? parseDiagnostics
166
167
  : valid
167
168
  ? []
168
- : (validateDocument.errors ?? []).map((error) => {
169
+ : (validateDocument().errors ?? []).map((error) => {
169
170
  const property = error.keyword === 'additionalProperties'
170
171
  ? String(error.params.additionalProperty)
171
172
  : undefined;
@@ -330,8 +331,8 @@ function compileWorkspaceResolved(parsed) {
330
331
  profileDiagnostics.push(...entry.schemaDiagnostics);
331
332
  continue;
332
333
  }
333
- if (!validateProfile(value)) {
334
- for (const error of validateProfile.errors ?? []) {
334
+ if (!validateProfile()(value)) {
335
+ for (const error of validateProfile().errors ?? []) {
335
336
  const property = error.keyword === 'additionalProperties'
336
337
  ? String(error.params.additionalProperty)
337
338
  : undefined;
@@ -362,9 +363,22 @@ function compileWorkspaceResolved(parsed) {
362
363
  }
363
364
  const alreadyDeclaresPolicy = pendingProfiles.some(({ identity }) => identity === shippedPolicyIdentity);
364
365
  if (!alreadyDeclaresPolicy) {
366
+ // This probe runs BEFORE the document gate that rejects a source whose
367
+ // schema check failed, so it has to hold its own precondition: a source
368
+ // that composes to anything but a mapping - an empty file, a comment-only
369
+ // one, a bare scalar - selects no profile at all. It used to read
370
+ // `.profile` through an `as` cast, which is what hid the null from the
371
+ // typechecker, and an empty document crashed the whole compile with a
372
+ // `TypeError` instead of the `YM201 must be object` its schema already
373
+ // produces. Every other consumer of a parsed entry checks its diagnostics
374
+ // first (the profile walk above, the pattern walk below); this one could
375
+ // not, because it runs before that gate exists, so it narrows instead.
365
376
  const selected = documentInputs.some(({ entry }) => {
366
377
  const value = entry.value;
367
- return value.profile === shippedPolicyIdentity;
378
+ return (typeof value === 'object' &&
379
+ value !== null &&
380
+ value.profile ===
381
+ shippedPolicyIdentity);
368
382
  });
369
383
  const extended = pendingProfiles.some(({ value }) => value.extends === shippedPolicyIdentity);
370
384
  if (selected || extended) {
@@ -377,8 +391,8 @@ function compileWorkspaceResolved(parsed) {
377
391
  if (entry.schemaDiagnostics.length > 0) {
378
392
  profileDiagnostics.push(...entry.schemaDiagnostics);
379
393
  }
380
- else if (!validateProfile(value)) {
381
- for (const error of validateProfile.errors ?? []) {
394
+ else if (!validateProfile()(value)) {
395
+ for (const error of validateProfile().errors ?? []) {
382
396
  profileDiagnostics.push({
383
397
  severity: 'error',
384
398
  code: 'YM201',
@@ -627,8 +641,8 @@ function compileWorkspaceResolved(parsed) {
627
641
  patternDiagnostics.push(...entry.schemaDiagnostics);
628
642
  continue;
629
643
  }
630
- if (!validatePattern(value)) {
631
- for (const error of validatePattern.errors ?? []) {
644
+ if (!validatePattern()(value)) {
645
+ for (const error of validatePattern().errors ?? []) {
632
646
  const property = error.keyword === 'additionalProperties'
633
647
  ? String(error.params.additionalProperty)
634
648
  : undefined;
@@ -1,6 +1,7 @@
1
1
  import Ajv2020Module from 'ajv/dist/2020.js';
2
2
  import { LineCounter, parseDocument } from 'yaml';
3
3
  import { diagnosticOrder, loadSourceDocument, locateSourcePath, } from './source-document.js';
4
+ import { lazyValidator } from './schema-validation.js';
4
5
  import coreContractSchema from '../schema/yarramate-core-contract.schema.json' with { type: 'json'
5
6
  };
6
7
  // `.default ?? module`, not a bare `.default`: NodeNext sees the raw CJS
@@ -9,9 +10,9 @@ import coreContractSchema from '../schema/yarramate-core-contract.schema.json' w
9
10
  // to keep track of (#252).
10
11
  const ajv2020Module = Ajv2020Module;
11
12
  const Ajv2020 = ajv2020Module.default ?? ajv2020Module;
12
- const validateCoreContract = new Ajv2020({ allErrors: true }).compile(coreContractSchema);
13
+ const validateCoreContract = lazyValidator(() => new Ajv2020({ allErrors: true }).compile(coreContractSchema));
13
14
  export function loadCoreContract(source) {
14
- const loaded = loadSourceDocument(source, validateCoreContract, 'Core contract');
15
+ const loaded = loadSourceDocument(source, validateCoreContract(), 'Core contract');
15
16
  if (!loaded.ok)
16
17
  return loaded;
17
18
  const { value, yaml, lineCounter } = loaded.document;
package/dist/evidence.js CHANGED
@@ -8,11 +8,12 @@ import evidenceSchema from '../schema/yarramate-evidence.schema.json' with { typ
8
8
  // to keep track of (#252).
9
9
  const ajv2020Module = Ajv2020Module;
10
10
  const Ajv2020 = ajv2020Module.default ?? ajv2020Module;
11
- const validateEvidenceSchema = new Ajv2020({ allErrors: true }).compile(evidenceSchema);
11
+ import { lazyValidator } from './schema-validation.js';
12
+ const validateEvidenceSchema = lazyValidator(() => new Ajv2020({ allErrors: true }).compile(evidenceSchema));
12
13
  const evidenceLocations = new WeakMap();
13
14
  const observationTarget = (observation) => 'subject' in observation ? observation.subject : observation.claim;
14
15
  export function loadEvidence(source) {
15
- const loaded = loadSourceDocument(source, validateEvidenceSchema, 'Evidence');
16
+ const loaded = loadSourceDocument(source, validateEvidenceSchema(), 'Evidence');
16
17
  if (!loaded.ok)
17
18
  return loaded;
18
19
  const { value, yaml, lineCounter } = loaded.document;
@@ -10,7 +10,8 @@ import catalogueSchema from '../schema/yarramate-question-catalogue.schema.json'
10
10
  // to keep track of (#252).
11
11
  const ajv2020Module = Ajv2020Module;
12
12
  const Ajv2020 = ajv2020Module.default ?? ajv2020Module;
13
- const validateCatalogue = new Ajv2020({ allErrors: true }).compile(catalogueSchema);
13
+ import { lazyValidator } from './schema-validation.js';
14
+ const validateCatalogue = lazyValidator(() => new Ajv2020({ allErrors: true }).compile(catalogueSchema));
14
15
  /**
15
16
  * The version of condition evaluation itself, not of the package.
16
17
  *
@@ -916,7 +917,7 @@ const unauthorableOffers = (catalogue, profileContext) => {
916
917
  return found;
917
918
  };
918
919
  const loadCatalogueDocument = (catalogueSource) => {
919
- const loaded = loadSourceDocument(catalogueSource, validateCatalogue, 'Question catalogue');
920
+ const loaded = loadSourceDocument(catalogueSource, validateCatalogue(), 'Question catalogue');
920
921
  if (!loaded.ok)
921
922
  return { ok: false, diagnostics: loaded.diagnostics };
922
923
  return {
@@ -6,7 +6,7 @@ import projectionSchema from '../schema/yarramate-projection.schema.json' with {
6
6
  };
7
7
  const ajv2020Module = Ajv2020Import;
8
8
  const Ajv2020 = ajv2020Module.default ?? ajv2020Module;
9
- const validateProjection = new Ajv2020({ allErrors: true }).compile(projectionSchema);
9
+ const validateProjection = lazyValidator(() => new Ajv2020({ allErrors: true }).compile(projectionSchema));
10
10
  /**
11
11
  * A relationship kind a view may draw as nesting, and the default. Defined in
12
12
  * `./nesting.js`, which imports nothing, and re-exported here so a consumer
@@ -20,8 +20,9 @@ export { DEFAULT_NESTING } from './nesting.js';
20
20
  * nesting vocabulary above, and re-exported here on the same terms (ADR 0121).
21
21
  */
22
22
  export { DEFAULT_DIRECTION } from './layout-direction.js';
23
+ import { lazyValidator } from './schema-validation.js';
23
24
  export function loadProjection(source) {
24
- const loaded = loadSourceDocument(source, validateProjection, 'Projection');
25
+ const loaded = loadSourceDocument(source, validateProjection(), 'Projection');
25
26
  return loaded.ok
26
27
  ? { ok: true, projection: loaded.document.value }
27
28
  : loaded;
@@ -0,0 +1,30 @@
1
+ import type { ValidateFunction } from 'ajv';
2
+ /**
3
+ * Defers compiling a JSON Schema until something actually validates against
4
+ * it, then reuses the compiled validator forever.
5
+ *
6
+ * Ten validators used to be constructed and compiled at MODULE SCOPE, so
7
+ * importing the package paid for every one of them whether or not a caller
8
+ * ever validated anything. Measured on the published 1.15.1 dist: ten
9
+ * `Ajv.compile` calls costing **123.7ms, 80% of the barrel's entire import
10
+ * time**, and one more on the `yarramate/interrogation` subpath, which is the
11
+ * entry we tell Workers consumers to prefer. That is not an abstract cost:
12
+ * Cloudflare Workers budget STARTUP CPU separately from request CPU and refuse
13
+ * a Worker that exceeds it, so an adopter's deploy was rejected outright
14
+ * (error 10021) by work no request had asked for.
15
+ *
16
+ * Deferring moves that cost to first use, where the budget is seconds rather
17
+ * than milliseconds, and a caller pays only for the schemas it actually
18
+ * touches - a consumer that compiles a workspace no longer pays for the
19
+ * evidence, adapter-mapping and core-contract validators it never calls.
20
+ *
21
+ * The accessor is a function rather than a getter so the deferral is visible
22
+ * at every call site: `validateDocument()(value)` reads as "get the validator,
23
+ * then use it", and `validateDocument().errors` cannot accidentally be read
24
+ * off a validator that was never run. Ajv attaches `errors` to the validator
25
+ * itself, so the two must come from the same object.
26
+ *
27
+ * `test/schema-validation-laziness.test.ts` asserts that importing the package
28
+ * compiles NOTHING, and fails on the next module-scope validator anyone adds.
29
+ */
30
+ export declare const lazyValidator: <T = unknown>(compile: () => ValidateFunction<T>) => (() => ValidateFunction<T>);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Defers compiling a JSON Schema until something actually validates against
3
+ * it, then reuses the compiled validator forever.
4
+ *
5
+ * Ten validators used to be constructed and compiled at MODULE SCOPE, so
6
+ * importing the package paid for every one of them whether or not a caller
7
+ * ever validated anything. Measured on the published 1.15.1 dist: ten
8
+ * `Ajv.compile` calls costing **123.7ms, 80% of the barrel's entire import
9
+ * time**, and one more on the `yarramate/interrogation` subpath, which is the
10
+ * entry we tell Workers consumers to prefer. That is not an abstract cost:
11
+ * Cloudflare Workers budget STARTUP CPU separately from request CPU and refuse
12
+ * a Worker that exceeds it, so an adopter's deploy was rejected outright
13
+ * (error 10021) by work no request had asked for.
14
+ *
15
+ * Deferring moves that cost to first use, where the budget is seconds rather
16
+ * than milliseconds, and a caller pays only for the schemas it actually
17
+ * touches - a consumer that compiles a workspace no longer pays for the
18
+ * evidence, adapter-mapping and core-contract validators it never calls.
19
+ *
20
+ * The accessor is a function rather than a getter so the deferral is visible
21
+ * at every call site: `validateDocument()(value)` reads as "get the validator,
22
+ * then use it", and `validateDocument().errors` cannot accidentally be read
23
+ * off a validator that was never run. Ajv attaches `errors` to the validator
24
+ * itself, so the two must come from the same object.
25
+ *
26
+ * `test/schema-validation-laziness.test.ts` asserts that importing the package
27
+ * compiles NOTHING, and fails on the next module-scope validator anyone adds.
28
+ */
29
+ export const lazyValidator = (compile) => {
30
+ let compiled;
31
+ return () => (compiled ??= compile());
32
+ };
@@ -18,7 +18,13 @@ export type SubjectReferenceGroup = 'document' | 'projection' | 'evidence' | 'ad
18
18
  export type SubjectReferenceForm = 'declaration' | 'reference' | 'qualified';
19
19
  export interface SubjectReferencePosition {
20
20
  readonly group: SubjectReferenceGroup;
21
- /** Key path from the document root; `*` matches every sequence index. */
21
+ /**
22
+ * Key path from the document root; `*` matches every element of a
23
+ * collection, which is every index of a sequence and every value of a
24
+ * mapping whose keys are open. Both spell `*` because the schema, not the
25
+ * position, is what decides which one a path lands on: `items` and
26
+ * `patternProperties` are the same shape of "and now, each of these".
27
+ */
22
28
  readonly path: readonly string[];
23
29
  readonly form: SubjectReferenceForm;
24
30
  }
@@ -25,6 +25,15 @@ export const SUBJECT_REFERENCE_POSITIONS = [
25
25
  path: ['concepts', '*', 'constraints', '*', 'ref'],
26
26
  form: 'reference',
27
27
  },
28
+ // The subjects bound into a pattern instance's slots (ADR 0123). Keyed by
29
+ // slot name rather than indexed, which is why it went missing: this is the
30
+ // only address in any of the four schemas that lives in a mapping, and the
31
+ // completeness walker below derived only sequences.
32
+ {
33
+ group: 'document',
34
+ path: ['concepts', '*', 'parts', '*'],
35
+ form: 'reference',
36
+ },
28
37
  {
29
38
  group: 'document',
30
39
  path: ['concepts', '*', 'references', '*', 'ref'],
@@ -150,10 +159,22 @@ const collect = (node, path, pointer, form, documentId, source, hits, aliases) =
150
159
  }
151
160
  const [segment, ...rest] = path;
152
161
  if (segment === '*') {
153
- if (!isSeq(node))
162
+ if (isSeq(node)) {
163
+ for (const [index, item] of node.items.entries()) {
164
+ collect(item, rest, `${pointer}/${index}`, form, documentId, source, hits, aliases);
165
+ }
154
166
  return;
155
- for (const [index, item] of node.items.entries()) {
156
- collect(item, rest, `${pointer}/${index}`, form, documentId, source, hits, aliases);
167
+ }
168
+ // A mapping with open keys, which today is `parts` alone. The pointer
169
+ // segment is the authored key, so a diagnostic names the SLOT ("/parts/
170
+ // interface") rather than a position the reader would have to count out.
171
+ if (isMap(node)) {
172
+ for (const item of node.items) {
173
+ const key = isScalar(item.key) ? String(item.key.value) : undefined;
174
+ if (key === undefined)
175
+ continue;
176
+ collect(item.value, rest, `${pointer}/${key}`, form, documentId, source, hits, aliases);
177
+ }
157
178
  }
158
179
  return;
159
180
  }