@neurodock/core 0.0.1 → 0.2.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.
Files changed (57) hide show
  1. package/LICENSE +657 -7
  2. package/README.md +50 -3
  3. package/data/neurotype-addenda/v1.json +389 -0
  4. package/dist/index.d.ts +5 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +7 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/index.test.d.ts +2 -0
  9. package/dist/index.test.d.ts.map +1 -0
  10. package/dist/index.test.js +29 -0
  11. package/dist/index.test.js.map +1 -0
  12. package/dist/neurotype-addenda.d.ts +91 -0
  13. package/dist/neurotype-addenda.d.ts.map +1 -0
  14. package/dist/neurotype-addenda.js +175 -0
  15. package/dist/neurotype-addenda.js.map +1 -0
  16. package/dist/neurotype-addenda.parity.test.d.ts +2 -0
  17. package/dist/neurotype-addenda.parity.test.d.ts.map +1 -0
  18. package/dist/neurotype-addenda.parity.test.js +69 -0
  19. package/dist/neurotype-addenda.parity.test.js.map +1 -0
  20. package/dist/neurotype-addenda.schema.test.d.ts +2 -0
  21. package/dist/neurotype-addenda.schema.test.d.ts.map +1 -0
  22. package/dist/neurotype-addenda.schema.test.js +83 -0
  23. package/dist/neurotype-addenda.schema.test.js.map +1 -0
  24. package/dist/neurotype-addenda.test.d.ts +2 -0
  25. package/dist/neurotype-addenda.test.d.ts.map +1 -0
  26. package/dist/neurotype-addenda.test.js +165 -0
  27. package/dist/neurotype-addenda.test.js.map +1 -0
  28. package/dist/profile.d.ts +128 -0
  29. package/dist/profile.d.ts.map +1 -0
  30. package/dist/profile.js +27 -0
  31. package/dist/profile.js.map +1 -0
  32. package/dist/profile.presets.test.d.ts +2 -0
  33. package/dist/profile.presets.test.d.ts.map +1 -0
  34. package/dist/profile.presets.test.js +43 -0
  35. package/dist/profile.presets.test.js.map +1 -0
  36. package/dist/profile.schema.test.d.ts +2 -0
  37. package/dist/profile.schema.test.d.ts.map +1 -0
  38. package/dist/profile.schema.test.js +282 -0
  39. package/dist/profile.schema.test.js.map +1 -0
  40. package/dist/profile.test.d.ts +2 -0
  41. package/dist/profile.test.d.ts.map +1 -0
  42. package/dist/profile.test.js +60 -0
  43. package/dist/profile.test.js.map +1 -0
  44. package/dist/test-helpers/ajv.d.ts +22 -0
  45. package/dist/test-helpers/ajv.d.ts.map +1 -0
  46. package/dist/test-helpers/ajv.js +38 -0
  47. package/dist/test-helpers/ajv.js.map +1 -0
  48. package/package.json +16 -9
  49. package/schemas/neurotype-addenda.schema.json +167 -0
  50. package/schemas/plugin.example.yaml +116 -71
  51. package/schemas/plugin.minimal.yaml +18 -12
  52. package/schemas/plugin.schema.json +13 -2
  53. package/schemas/profile.example.yaml +112 -64
  54. package/schemas/profile.minimal.yaml +15 -7
  55. package/schemas/profile.schema.json +116 -0
  56. package/src/index.test.ts +0 -8
  57. package/src/index.ts +0 -1
@@ -0,0 +1,128 @@
1
+ /**
2
+ * profile.ts — the canonical TypeScript shape of a NeuroDock profile.
3
+ *
4
+ * Derived by hand from `schemas/profile.schema.json` and linked to it by the
5
+ * runtime-assertion test in `profile.test.ts`, which validates representative
6
+ * typed fixtures against the Ajv-compiled schema so the two cannot drift.
7
+ *
8
+ * ADR 0004 says shared types SHOULD be derived from the schema. We deliberately
9
+ * hand-write this type and close the drift loop with the runtime schema-assertion
10
+ * test above, rather than running a codegen step: it keeps `@neurodock/core`'s
11
+ * build a plain `tsc` with zero codegen tooling and zero runtime deps, and the
12
+ * surface is small enough that the assertion test is a cheaper, equally reliable
13
+ * guard. This is an intentional choice, not an oversight.
14
+ *
15
+ * Forward-compat (ADR 0004): unknown keys are allowed at every object level,
16
+ * so each interface carries an index signature. Every field below `identity`
17
+ * is optional; defaults are loader-applied, never written into the file.
18
+ *
19
+ * Additive-only (ADR 0005 / ADR 0011): the R5 tailoring fields are OPTIONAL
20
+ * and additive. They are never required and never narrow an existing type.
21
+ */
22
+ /** Self-identified neurotype tags. Self-ID is sufficient; never a diagnosis. */
23
+ export type Neurotype = "adhd" | "asd" | "audhd" | "ocd" | "dyslexia" | "dyspraxia" | "tourette" | "other";
24
+ export type OutputFormat = "answer_first" | "conventional" | "bullet_first";
25
+ export type ReadingFontHint = "atkinson_hyperlegible" | "lexend" | "system_default";
26
+ export type Motion = "reduced" | "system" | "full";
27
+ /** R5 (ADR 0011): categorical line-height band for rich-text clients. */
28
+ export type LineHeightHint = "compact" | "default" | "relaxed";
29
+ export type SessionOverlapPolicy = "auto_close" | "error";
30
+ /** R5 (ADR 0011): self-declared term/semester phase. */
31
+ export type CalendarPhase = "teaching" | "marking" | "exam" | "deadlines" | "break";
32
+ export type SycophancyCheck = "off" | "warn" | "refuse";
33
+ export type Embeddings = "local" | "cloud_voyage" | "cloud_openai";
34
+ export type Telemetry = "off" | "local_otel_only" | "full";
35
+ /** Lowercase English weekday names — the keys of `weekday_overrides`. */
36
+ export type Weekday = "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday";
37
+ export interface ProfileIdentity {
38
+ display_name: string;
39
+ neurotypes: Neurotype[];
40
+ additional_notes?: string;
41
+ [key: string]: unknown;
42
+ }
43
+ export interface ProfilePreferences {
44
+ output_format?: OutputFormat;
45
+ max_chunk_size?: number;
46
+ reading_font_hint?: ReadingFontHint;
47
+ motion?: Motion;
48
+ /** R5 (ADR 0011): optional rich-text line-height hint. */
49
+ line_height_hint?: LineHeightHint;
50
+ /** R5 (ADR 0011): true when the user predominantly dictates input. */
51
+ voice_input_preferred?: boolean;
52
+ [key: string]: unknown;
53
+ }
54
+ /**
55
+ * R5 (ADR 0011): per-weekday override of the day-varying chronometric fields.
56
+ *
57
+ * Intentionally NO `[key: string]: unknown` index signature, unlike the other
58
+ * interfaces in this file. This corresponds to the `weekdayOverride` `$def`,
59
+ * which sets `additionalProperties: false` as a deliberate typo-guard (a
60
+ * misspelt key here is almost always a mistake that would silently do nothing).
61
+ * Omitting the index signature mirrors that closed shape in the type; do NOT
62
+ * "fix" it by adding one — the schema would then reject what the type permits.
63
+ */
64
+ export interface WeekdayOverride {
65
+ end_of_day_local?: string;
66
+ hyperfocus_break_minutes?: number;
67
+ }
68
+ /**
69
+ * R5 (ADR 0011): a local-time range the hyperfocus monitor hard-surfaces in.
70
+ *
71
+ * Intentionally NO `[key: string]: unknown` index signature, unlike the other
72
+ * interfaces in this file. This corresponds to the `protectedWindow` `$def`,
73
+ * which sets `additionalProperties: false` as a deliberate typo-guard for its
74
+ * small, fixed shape. Omitting the index signature mirrors that closed shape;
75
+ * do NOT "fix" it by adding one — the schema would then reject what the type
76
+ * permits.
77
+ */
78
+ export interface ProtectedWindow {
79
+ start: string;
80
+ end: string;
81
+ label?: string;
82
+ }
83
+ export interface ProfileChronometric {
84
+ hyperfocus_break_minutes?: number;
85
+ end_of_day_local?: string;
86
+ zones?: Record<string, unknown>;
87
+ session_overlap_policy?: SessionOverlapPolicy;
88
+ /** R5 (ADR 0011): self-declared term/semester phase. */
89
+ calendar_phase?: CalendarPhase;
90
+ /** R5 (ADR 0011): per-weekday overrides; absent weekdays inherit top-level. */
91
+ weekday_overrides?: Partial<Record<Weekday, WeekdayOverride>>;
92
+ /** R5 (ADR 0011): windows where the monitor hard-surfaces rather than nudges. */
93
+ protected_windows?: ProtectedWindow[];
94
+ /** R5 (ADR 0011): surface deadline proximity in planning skills. */
95
+ deadline_cluster_awareness?: boolean;
96
+ /** R5 (ADR 0011): pad presented time estimates (1.0..3.0; neutral 1.0). */
97
+ time_buffer_multiplier?: number;
98
+ /** R5 (ADR 0011): weight motor activity into the fatigue signal. */
99
+ motor_fatigue_aware?: boolean;
100
+ [key: string]: unknown;
101
+ }
102
+ export interface ProfileGuardrails {
103
+ rumination_threshold?: number;
104
+ rumination_window_minutes?: number;
105
+ sycophancy_check?: SycophancyCheck;
106
+ [key: string]: unknown;
107
+ }
108
+ export interface ProfilePrivacy {
109
+ embeddings?: Embeddings;
110
+ telemetry?: Telemetry;
111
+ os_idle_consent?: boolean;
112
+ [key: string]: unknown;
113
+ }
114
+ /**
115
+ * A NeuroDock profile. `identity` is the only required block; every other
116
+ * block is optional and defaulted by the loader at read time (ADR 0004).
117
+ */
118
+ export interface Profile {
119
+ schema_version?: string;
120
+ extends?: string;
121
+ identity: ProfileIdentity;
122
+ preferences?: ProfilePreferences;
123
+ chronometric?: ProfileChronometric;
124
+ guardrails?: ProfileGuardrails;
125
+ privacy?: ProfilePrivacy;
126
+ [key: string]: unknown;
127
+ }
128
+ //# sourceMappingURL=profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../src/profile.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /*
2
+ * SPDX-License-Identifier: AGPL-3.0-or-later
3
+ * Copyright (c) 2026 NeuroDock contributors.
4
+ */
5
+ /**
6
+ * profile.ts — the canonical TypeScript shape of a NeuroDock profile.
7
+ *
8
+ * Derived by hand from `schemas/profile.schema.json` and linked to it by the
9
+ * runtime-assertion test in `profile.test.ts`, which validates representative
10
+ * typed fixtures against the Ajv-compiled schema so the two cannot drift.
11
+ *
12
+ * ADR 0004 says shared types SHOULD be derived from the schema. We deliberately
13
+ * hand-write this type and close the drift loop with the runtime schema-assertion
14
+ * test above, rather than running a codegen step: it keeps `@neurodock/core`'s
15
+ * build a plain `tsc` with zero codegen tooling and zero runtime deps, and the
16
+ * surface is small enough that the assertion test is a cheaper, equally reliable
17
+ * guard. This is an intentional choice, not an oversight.
18
+ *
19
+ * Forward-compat (ADR 0004): unknown keys are allowed at every object level,
20
+ * so each interface carries an index signature. Every field below `identity`
21
+ * is optional; defaults are loader-applied, never written into the file.
22
+ *
23
+ * Additive-only (ADR 0005 / ADR 0011): the R5 tailoring fields are OPTIONAL
24
+ * and additive. They are never required and never narrow an existing type.
25
+ */
26
+ export {};
27
+ //# sourceMappingURL=profile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.js","sourceRoot":"","sources":["../src/profile.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH;;;;;;;;;;;;;;;;;;;;GAoBG"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=profile.presets.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.presets.test.d.ts","sourceRoot":"","sources":["../src/profile.presets.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,43 @@
1
+ /*
2
+ * SPDX-License-Identifier: AGPL-3.0-or-later
3
+ * Copyright (c) 2026 NeuroDock contributors.
4
+ */
5
+ /**
6
+ * profile.presets.test.ts — every curated preset under the repo-root
7
+ * `profiles/` directory MUST validate against the canonical profile schema.
8
+ * Presets are part of the public contract (ADR 0004): a preset that drifts
9
+ * out of conformance is a user-facing breakage. This walks the directory so
10
+ * a newly added preset is covered automatically.
11
+ */
12
+ import { readFileSync, readdirSync } from "node:fs";
13
+ import { fileURLToPath } from "node:url";
14
+ import { dirname, resolve } from "node:path";
15
+ import { parse as parseYaml } from "yaml";
16
+ import { beforeAll, describe, expect, test } from "vitest";
17
+ import { buildAjv } from "./test-helpers/ajv.js";
18
+ const here = dirname(fileURLToPath(import.meta.url));
19
+ const repoRoot = resolve(here, "..", "..", "..");
20
+ const presetsDir = resolve(repoRoot, "profiles");
21
+ function readSchema() {
22
+ const raw = readFileSync(resolve(here, "..", "schemas", "profile.schema.json"), "utf8");
23
+ return JSON.parse(raw);
24
+ }
25
+ let validate;
26
+ beforeAll(() => {
27
+ validate = buildAjv().compile(readSchema());
28
+ });
29
+ const presetFiles = readdirSync(presetsDir).filter((f) => f.endsWith(".yaml"));
30
+ describe("curated presets validate against the profile schema", () => {
31
+ test("the profiles directory contains presets to check", () => {
32
+ expect(presetFiles.length).toBeGreaterThan(0);
33
+ });
34
+ test.each(presetFiles)("%s is a valid profile", (file) => {
35
+ const parsed = parseYaml(readFileSync(resolve(presetsDir, file), "utf8"));
36
+ const ok = validate(parsed);
37
+ if (!ok) {
38
+ throw new Error(`${file} failed schema validation: ${JSON.stringify(validate.errors, null, 2)}`);
39
+ }
40
+ expect(ok).toBe(true);
41
+ });
42
+ });
43
+ //# sourceMappingURL=profile.presets.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.presets.test.js","sourceRoot":"","sources":["../src/profile.presets.test.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH;;;;;;GAMG;AACH,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE3D,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEjD,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACrD,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AACjD,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AAEjD,SAAS,UAAU;IACjB,MAAM,GAAG,GAAG,YAAY,CACtB,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,qBAAqB,CAAC,EACrD,MAAM,CACP,CAAC;IACF,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAW,CAAC;AACnC,CAAC;AAED,IAAI,QAA0B,CAAC;AAE/B,SAAS,CAAC,GAAG,EAAE;IACb,QAAQ,GAAG,QAAQ,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;AAC9C,CAAC,CAAC,CAAC;AAEH,MAAM,WAAW,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;AAE/E,QAAQ,CAAC,qDAAqD,EAAE,GAAG,EAAE;IACnE,IAAI,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC5D,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,uBAAuB,EAAE,CAAC,IAAI,EAAE,EAAE;QACvD,MAAM,MAAM,GAAG,SAAS,CAAC,YAAY,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QAC1E,MAAM,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC5B,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,8BAA8B,IAAI,CAAC,SAAS,CACjD,QAAQ,CAAC,MAAM,EACf,IAAI,EACJ,CAAC,CACF,EAAE,CACJ,CAAC;QACJ,CAAC;QACD,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=profile.schema.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.schema.test.d.ts","sourceRoot":"","sources":["../src/profile.schema.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,282 @@
1
+ /*
2
+ * SPDX-License-Identifier: AGPL-3.0-or-later
3
+ * Copyright (c) 2026 NeuroDock contributors.
4
+ */
5
+ /**
6
+ * profile.schema.test.ts — schema-conformance gate for the NeuroDock profile.
7
+ *
8
+ * Compiles the canonical schema (the exact file shipped in the package and
9
+ * consumed at runtime by @neurodock/cli, the extension, and the native host)
10
+ * with Ajv 2020 and asserts:
11
+ * - the worked example + minimal templates validate clean (they are part of
12
+ * the contract per ADR 0004);
13
+ * - every R5 optional field (ADR 0011) validates when present, is valid when
14
+ * omitted, and is rejected on a malformed value;
15
+ * - a v0.1.0 profile carrying NONE of the new fields still validates
16
+ * (backward-compat — the central correctness property of ADR 0004).
17
+ *
18
+ * Ajv + ajv-formats + yaml are dev-only test dependencies. The package keeps
19
+ * its zero-runtime-dependencies invariant.
20
+ */
21
+ import { readFileSync } from "node:fs";
22
+ import { fileURLToPath } from "node:url";
23
+ import { dirname, resolve } from "node:path";
24
+ import { parse as parseYaml } from "yaml";
25
+ import { beforeAll, describe, expect, test } from "vitest";
26
+ import { buildAjv } from "./test-helpers/ajv.js";
27
+ const here = dirname(fileURLToPath(import.meta.url));
28
+ const schemasDir = resolve(here, "..", "schemas");
29
+ function readSchema() {
30
+ const raw = readFileSync(resolve(schemasDir, "profile.schema.json"), "utf8");
31
+ return JSON.parse(raw);
32
+ }
33
+ function readYaml(name) {
34
+ return parseYaml(readFileSync(resolve(schemasDir, name), "utf8"));
35
+ }
36
+ let validate;
37
+ beforeAll(() => {
38
+ validate = buildAjv().compile(readSchema());
39
+ });
40
+ const BASE_IDENTITY = {
41
+ identity: { display_name: "T", neurotypes: ["adhd"] },
42
+ };
43
+ describe("profile schema — shipped templates", () => {
44
+ test("the worked example validates clean", () => {
45
+ expect(validate(readYaml("profile.example.yaml"))).toBe(true);
46
+ });
47
+ test("the minimal template validates clean", () => {
48
+ expect(validate(readYaml("profile.minimal.yaml"))).toBe(true);
49
+ });
50
+ });
51
+ describe("profile schema — backward compatibility (ADR 0004)", () => {
52
+ test("a v0.1.0 profile carrying NONE of the R5 fields still validates", () => {
53
+ const legacy = {
54
+ schema_version: "0.1.0",
55
+ identity: { display_name: "T", neurotypes: ["adhd", "asd"] },
56
+ preferences: {
57
+ output_format: "answer_first",
58
+ max_chunk_size: 5,
59
+ reading_font_hint: "atkinson_hyperlegible",
60
+ motion: "reduced",
61
+ },
62
+ chronometric: {
63
+ hyperfocus_break_minutes: 90,
64
+ end_of_day_local: "18:30",
65
+ session_overlap_policy: "auto_close",
66
+ },
67
+ guardrails: {
68
+ rumination_threshold: 3,
69
+ rumination_window_minutes: 90,
70
+ sycophancy_check: "warn",
71
+ },
72
+ privacy: {
73
+ embeddings: "local",
74
+ telemetry: "off",
75
+ os_idle_consent: false,
76
+ },
77
+ };
78
+ expect(validate(legacy)).toBe(true);
79
+ });
80
+ test("the bare minimal profile (identity only) is still valid", () => {
81
+ expect(validate({ ...BASE_IDENTITY })).toBe(true);
82
+ });
83
+ });
84
+ describe("preferences.line_height_hint (R5)", () => {
85
+ test.each(["compact", "default", "relaxed"])("accepts the enum value %s", (value) => {
86
+ expect(validate({
87
+ ...BASE_IDENTITY,
88
+ preferences: { line_height_hint: value },
89
+ })).toBe(true);
90
+ });
91
+ test("is valid when omitted", () => {
92
+ expect(validate({ ...BASE_IDENTITY, preferences: {} })).toBe(true);
93
+ });
94
+ test("rejects a value outside the enum", () => {
95
+ expect(validate({
96
+ ...BASE_IDENTITY,
97
+ preferences: { line_height_hint: "double" },
98
+ })).toBe(false);
99
+ });
100
+ });
101
+ describe("preferences.voice_input_preferred (R5)", () => {
102
+ test("accepts true", () => {
103
+ expect(validate({
104
+ ...BASE_IDENTITY,
105
+ preferences: { voice_input_preferred: true },
106
+ })).toBe(true);
107
+ });
108
+ test("is valid when omitted", () => {
109
+ expect(validate({ ...BASE_IDENTITY, preferences: {} })).toBe(true);
110
+ });
111
+ test("rejects a non-boolean", () => {
112
+ expect(validate({
113
+ ...BASE_IDENTITY,
114
+ preferences: { voice_input_preferred: "yes" },
115
+ })).toBe(false);
116
+ });
117
+ });
118
+ describe("chronometric.calendar_phase (R5)", () => {
119
+ test.each(["teaching", "marking", "exam", "deadlines", "break"])("accepts the enum value %s", (value) => {
120
+ expect(validate({ ...BASE_IDENTITY, chronometric: { calendar_phase: value } })).toBe(true);
121
+ });
122
+ test("is valid when omitted", () => {
123
+ expect(validate({ ...BASE_IDENTITY, chronometric: {} })).toBe(true);
124
+ });
125
+ test("rejects a value outside the enum", () => {
126
+ expect(validate({
127
+ ...BASE_IDENTITY,
128
+ chronometric: { calendar_phase: "summer" },
129
+ })).toBe(false);
130
+ });
131
+ });
132
+ describe("chronometric.weekday_overrides (R5)", () => {
133
+ test("accepts per-weekday overrides reusing the existing patterns/ranges", () => {
134
+ expect(validate({
135
+ ...BASE_IDENTITY,
136
+ chronometric: {
137
+ weekday_overrides: {
138
+ wednesday: { end_of_day_local: "18:30" },
139
+ saturday: { hyperfocus_break_minutes: 120 },
140
+ monday: {
141
+ end_of_day_local: "17:00",
142
+ hyperfocus_break_minutes: 60,
143
+ },
144
+ },
145
+ },
146
+ })).toBe(true);
147
+ });
148
+ test("accepts an empty override object for a weekday", () => {
149
+ expect(validate({
150
+ ...BASE_IDENTITY,
151
+ chronometric: { weekday_overrides: { friday: {} } },
152
+ })).toBe(true);
153
+ });
154
+ test("is valid when omitted", () => {
155
+ expect(validate({ ...BASE_IDENTITY, chronometric: {} })).toBe(true);
156
+ });
157
+ test("rejects an unknown weekday key", () => {
158
+ expect(validate({
159
+ ...BASE_IDENTITY,
160
+ chronometric: { weekday_overrides: { funday: {} } },
161
+ })).toBe(false);
162
+ });
163
+ test("rejects a malformed end_of_day_local inside an override", () => {
164
+ expect(validate({
165
+ ...BASE_IDENTITY,
166
+ chronometric: {
167
+ weekday_overrides: { monday: { end_of_day_local: "25:00" } },
168
+ },
169
+ })).toBe(false);
170
+ });
171
+ test("rejects an out-of-range hyperfocus_break_minutes inside an override", () => {
172
+ expect(validate({
173
+ ...BASE_IDENTITY,
174
+ chronometric: {
175
+ weekday_overrides: { monday: { hyperfocus_break_minutes: 5 } },
176
+ },
177
+ })).toBe(false);
178
+ });
179
+ test("rejects an unknown key inside an override object", () => {
180
+ expect(validate({
181
+ ...BASE_IDENTITY,
182
+ chronometric: { weekday_overrides: { monday: { lunch: "12:00" } } },
183
+ })).toBe(false);
184
+ });
185
+ });
186
+ describe("chronometric.protected_windows (R5)", () => {
187
+ test("accepts a list of local-time ranges with optional labels", () => {
188
+ expect(validate({
189
+ ...BASE_IDENTITY,
190
+ chronometric: {
191
+ protected_windows: [
192
+ { start: "12:00", end: "12:30", label: "lunch" },
193
+ { start: "17:00", end: "23:59" },
194
+ ],
195
+ },
196
+ })).toBe(true);
197
+ });
198
+ test("accepts an empty list", () => {
199
+ expect(validate({ ...BASE_IDENTITY, chronometric: { protected_windows: [] } })).toBe(true);
200
+ });
201
+ test("is valid when omitted", () => {
202
+ expect(validate({ ...BASE_IDENTITY, chronometric: {} })).toBe(true);
203
+ });
204
+ test("rejects a window missing the required end", () => {
205
+ expect(validate({
206
+ ...BASE_IDENTITY,
207
+ chronometric: { protected_windows: [{ start: "12:00" }] },
208
+ })).toBe(false);
209
+ });
210
+ test("rejects a malformed time string", () => {
211
+ expect(validate({
212
+ ...BASE_IDENTITY,
213
+ chronometric: { protected_windows: [{ start: "9am", end: "10am" }] },
214
+ })).toBe(false);
215
+ });
216
+ test("rejects an unknown key inside a window object", () => {
217
+ expect(validate({
218
+ ...BASE_IDENTITY,
219
+ chronometric: {
220
+ protected_windows: [{ start: "12:00", end: "12:30", color: "red" }],
221
+ },
222
+ })).toBe(false);
223
+ });
224
+ });
225
+ describe("chronometric.deadline_cluster_awareness (R5)", () => {
226
+ test("accepts true", () => {
227
+ expect(validate({
228
+ ...BASE_IDENTITY,
229
+ chronometric: { deadline_cluster_awareness: true },
230
+ })).toBe(true);
231
+ });
232
+ test("is valid when omitted", () => {
233
+ expect(validate({ ...BASE_IDENTITY, chronometric: {} })).toBe(true);
234
+ });
235
+ test("rejects a non-boolean", () => {
236
+ expect(validate({
237
+ ...BASE_IDENTITY,
238
+ chronometric: { deadline_cluster_awareness: "yes" },
239
+ })).toBe(false);
240
+ });
241
+ });
242
+ describe("chronometric.time_buffer_multiplier (R5)", () => {
243
+ test.each([1.0, 1.3, 3.0])("accepts the multiplier %s", (value) => {
244
+ expect(validate({
245
+ ...BASE_IDENTITY,
246
+ chronometric: { time_buffer_multiplier: value },
247
+ })).toBe(true);
248
+ });
249
+ test("is valid when omitted", () => {
250
+ expect(validate({ ...BASE_IDENTITY, chronometric: {} })).toBe(true);
251
+ });
252
+ test("rejects a multiplier below the minimum", () => {
253
+ expect(validate({
254
+ ...BASE_IDENTITY,
255
+ chronometric: { time_buffer_multiplier: 0.5 },
256
+ })).toBe(false);
257
+ });
258
+ test("rejects a multiplier above the maximum", () => {
259
+ expect(validate({
260
+ ...BASE_IDENTITY,
261
+ chronometric: { time_buffer_multiplier: 4 },
262
+ })).toBe(false);
263
+ });
264
+ });
265
+ describe("chronometric.motor_fatigue_aware (R5)", () => {
266
+ test("accepts true", () => {
267
+ expect(validate({
268
+ ...BASE_IDENTITY,
269
+ chronometric: { motor_fatigue_aware: true },
270
+ })).toBe(true);
271
+ });
272
+ test("is valid when omitted", () => {
273
+ expect(validate({ ...BASE_IDENTITY, chronometric: {} })).toBe(true);
274
+ });
275
+ test("rejects a non-boolean", () => {
276
+ expect(validate({
277
+ ...BASE_IDENTITY,
278
+ chronometric: { motor_fatigue_aware: 1 },
279
+ })).toBe(false);
280
+ });
281
+ });
282
+ //# sourceMappingURL=profile.schema.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.schema.test.js","sourceRoot":"","sources":["../src/profile.schema.test.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE3D,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEjD,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACrD,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;AAElD,SAAS,UAAU;IACjB,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,CAAC,UAAU,EAAE,qBAAqB,CAAC,EAAE,MAAM,CAAC,CAAC;IAC7E,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAW,CAAC;AACnC,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,SAAS,CAAC,YAAY,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AACpE,CAAC;AAED,IAAI,QAA0B,CAAC;AAE/B,SAAS,CAAC,GAAG,EAAE;IACb,QAAQ,GAAG,QAAQ,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;AAC9C,CAAC,CAAC,CAAC;AAEH,MAAM,aAAa,GAAG;IACpB,QAAQ,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,MAAM,CAAC,EAAE;CAC7C,CAAC;AAEX,QAAQ,CAAC,oCAAoC,EAAE,GAAG,EAAE;IAClD,IAAI,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAChD,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,oDAAoD,EAAE,GAAG,EAAE;IAClE,IAAI,CAAC,iEAAiE,EAAE,GAAG,EAAE;QAC3E,MAAM,MAAM,GAAG;YACb,cAAc,EAAE,OAAO;YACvB,QAAQ,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE;YAC5D,WAAW,EAAE;gBACX,aAAa,EAAE,cAAc;gBAC7B,cAAc,EAAE,CAAC;gBACjB,iBAAiB,EAAE,uBAAuB;gBAC1C,MAAM,EAAE,SAAS;aAClB;YACD,YAAY,EAAE;gBACZ,wBAAwB,EAAE,EAAE;gBAC5B,gBAAgB,EAAE,OAAO;gBACzB,sBAAsB,EAAE,YAAY;aACrC;YACD,UAAU,EAAE;gBACV,oBAAoB,EAAE,CAAC;gBACvB,yBAAyB,EAAE,EAAE;gBAC7B,gBAAgB,EAAE,MAAM;aACzB;YACD,OAAO,EAAE;gBACP,UAAU,EAAE,OAAO;gBACnB,SAAS,EAAE,KAAK;gBAChB,eAAe,EAAE,KAAK;aACvB;SACF,CAAC;QACF,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACnE,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,aAAa,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,mCAAmC,EAAE,GAAG,EAAE;IACjD,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC,CAC1C,2BAA2B,EAC3B,CAAC,KAAK,EAAE,EAAE;QACR,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,WAAW,EAAE,EAAE,gBAAgB,EAAE,KAAK,EAAE;SACzC,CAAC,CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC,CACF,CAAC;IAEF,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,aAAa,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC5C,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,WAAW,EAAE,EAAE,gBAAgB,EAAE,QAAQ,EAAE;SAC5C,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,wCAAwC,EAAE,GAAG,EAAE;IACtD,IAAI,CAAC,cAAc,EAAE,GAAG,EAAE;QACxB,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,WAAW,EAAE,EAAE,qBAAqB,EAAE,IAAI,EAAE;SAC7C,CAAC,CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,aAAa,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACjC,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,WAAW,EAAE,EAAE,qBAAqB,EAAE,KAAK,EAAE;SAC9C,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,kCAAkC,EAAE,GAAG,EAAE;IAChD,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,CAC9D,2BAA2B,EAC3B,CAAC,KAAK,EAAE,EAAE;QACR,MAAM,CACJ,QAAQ,CAAC,EAAE,GAAG,aAAa,EAAE,YAAY,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,EAAE,CAAC,CACxE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC,CACF,CAAC;IAEF,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,aAAa,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC5C,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE,EAAE,cAAc,EAAE,QAAQ,EAAE;SAC3C,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,qCAAqC,EAAE,GAAG,EAAE;IACnD,IAAI,CAAC,oEAAoE,EAAE,GAAG,EAAE;QAC9E,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE;gBACZ,iBAAiB,EAAE;oBACjB,SAAS,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE;oBACxC,QAAQ,EAAE,EAAE,wBAAwB,EAAE,GAAG,EAAE;oBAC3C,MAAM,EAAE;wBACN,gBAAgB,EAAE,OAAO;wBACzB,wBAAwB,EAAE,EAAE;qBAC7B;iBACF;aACF;SACF,CAAC,CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,gDAAgD,EAAE,GAAG,EAAE;QAC1D,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE,EAAE,iBAAiB,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE;SACpD,CAAC,CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,aAAa,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,gCAAgC,EAAE,GAAG,EAAE;QAC1C,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE,EAAE,iBAAiB,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE;SACpD,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACnE,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE;gBACZ,iBAAiB,EAAE,EAAE,MAAM,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE,EAAE;aAC7D;SACF,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,qEAAqE,EAAE,GAAG,EAAE;QAC/E,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE;gBACZ,iBAAiB,EAAE,EAAE,MAAM,EAAE,EAAE,wBAAwB,EAAE,CAAC,EAAE,EAAE;aAC/D;SACF,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC5D,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE,EAAE,iBAAiB,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE;SACpE,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,qCAAqC,EAAE,GAAG,EAAE;IACnD,IAAI,CAAC,0DAA0D,EAAE,GAAG,EAAE;QACpE,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE;gBACZ,iBAAiB,EAAE;oBACjB,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE;oBAChD,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE;iBACjC;aACF;SACF,CAAC,CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACjC,MAAM,CACJ,QAAQ,CAAC,EAAE,GAAG,aAAa,EAAE,YAAY,EAAE,EAAE,iBAAiB,EAAE,EAAE,EAAE,EAAE,CAAC,CACxE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,aAAa,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACrD,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE,EAAE,iBAAiB,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE;SAC1D,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,iCAAiC,EAAE,GAAG,EAAE;QAC3C,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE,EAAE,iBAAiB,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE;SACrE,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACzD,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE;gBACZ,iBAAiB,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;aACpE;SACF,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,8CAA8C,EAAE,GAAG,EAAE;IAC5D,IAAI,CAAC,cAAc,EAAE,GAAG,EAAE;QACxB,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE,EAAE,0BAA0B,EAAE,IAAI,EAAE;SACnD,CAAC,CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,aAAa,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACjC,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE,EAAE,0BAA0B,EAAE,KAAK,EAAE;SACpD,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,0CAA0C,EAAE,GAAG,EAAE;IACxD,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,2BAA2B,EAAE,CAAC,KAAK,EAAE,EAAE;QAChE,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE,EAAE,sBAAsB,EAAE,KAAK,EAAE;SAChD,CAAC,CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,aAAa,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAClD,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE,EAAE,sBAAsB,EAAE,GAAG,EAAE;SAC9C,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAClD,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE,EAAE,sBAAsB,EAAE,CAAC,EAAE;SAC5C,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,uCAAuC,EAAE,GAAG,EAAE;IACrD,IAAI,CAAC,cAAc,EAAE,GAAG,EAAE;QACxB,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE,EAAE,mBAAmB,EAAE,IAAI,EAAE;SAC5C,CAAC,CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACf,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,aAAa,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACjC,MAAM,CACJ,QAAQ,CAAC;YACP,GAAG,aAAa;YAChB,YAAY,EAAE,EAAE,mBAAmB,EAAE,CAAC,EAAE;SACzC,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=profile.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.test.d.ts","sourceRoot":"","sources":["../src/profile.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,60 @@
1
+ /*
2
+ * SPDX-License-Identifier: AGPL-3.0-or-later
3
+ * Copyright (c) 2026 NeuroDock contributors.
4
+ */
5
+ /**
6
+ * profile.test.ts — links the hand-written `Profile` type to the canonical
7
+ * JSON Schema. A typed fixture that exercises every R5 field is validated
8
+ * against the Ajv-compiled schema; if the type permits a shape the schema
9
+ * rejects (or vice versa) this test fails, preventing the two from drifting.
10
+ */
11
+ import { readFileSync } from "node:fs";
12
+ import { fileURLToPath } from "node:url";
13
+ import { dirname, resolve } from "node:path";
14
+ import { beforeAll, describe, expect, test } from "vitest";
15
+ import { buildAjv } from "./test-helpers/ajv.js";
16
+ const here = dirname(fileURLToPath(import.meta.url));
17
+ function readSchema() {
18
+ const raw = readFileSync(resolve(here, "..", "schemas", "profile.schema.json"), "utf8");
19
+ return JSON.parse(raw);
20
+ }
21
+ let validate;
22
+ beforeAll(() => {
23
+ validate = buildAjv().compile(readSchema());
24
+ });
25
+ describe("Profile type ↔ schema", () => {
26
+ test("a minimal typed Profile validates against the schema", () => {
27
+ const profile = {
28
+ identity: { display_name: "T", neurotypes: ["adhd"] },
29
+ };
30
+ expect(validate(profile)).toBe(true);
31
+ });
32
+ test("a typed Profile exercising every R5 field validates", () => {
33
+ const profile = {
34
+ schema_version: "0.1.0",
35
+ identity: { display_name: "T", neurotypes: ["dyspraxia"] },
36
+ preferences: {
37
+ output_format: "answer_first",
38
+ line_height_hint: "relaxed",
39
+ voice_input_preferred: true,
40
+ },
41
+ chronometric: {
42
+ hyperfocus_break_minutes: 60,
43
+ calendar_phase: "marking",
44
+ weekday_overrides: {
45
+ wednesday: { end_of_day_local: "18:30" },
46
+ saturday: { hyperfocus_break_minutes: 120 },
47
+ },
48
+ protected_windows: [
49
+ { start: "12:00", end: "12:30", label: "lunch" },
50
+ { start: "17:00", end: "23:59" },
51
+ ],
52
+ deadline_cluster_awareness: true,
53
+ time_buffer_multiplier: 1.3,
54
+ motor_fatigue_aware: true,
55
+ },
56
+ };
57
+ expect(validate(profile)).toBe(true);
58
+ });
59
+ });
60
+ //# sourceMappingURL=profile.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.test.js","sourceRoot":"","sources":["../src/profile.test.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE3D,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAGjD,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAErD,SAAS,UAAU;IACjB,MAAM,GAAG,GAAG,YAAY,CACtB,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,qBAAqB,CAAC,EACrD,MAAM,CACP,CAAC;IACF,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAW,CAAC;AACnC,CAAC;AAED,IAAI,QAA0B,CAAC;AAE/B,SAAS,CAAC,GAAG,EAAE;IACb,QAAQ,GAAG,QAAQ,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;AAC9C,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,IAAI,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAChE,MAAM,OAAO,GAAY;YACvB,QAAQ,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,MAAM,CAAC,EAAE;SACtD,CAAC;QACF,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC/D,MAAM,OAAO,GAAY;YACvB,cAAc,EAAE,OAAO;YACvB,QAAQ,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,WAAW,CAAC,EAAE;YAC1D,WAAW,EAAE;gBACX,aAAa,EAAE,cAAc;gBAC7B,gBAAgB,EAAE,SAAS;gBAC3B,qBAAqB,EAAE,IAAI;aAC5B;YACD,YAAY,EAAE;gBACZ,wBAAwB,EAAE,EAAE;gBAC5B,cAAc,EAAE,SAAS;gBACzB,iBAAiB,EAAE;oBACjB,SAAS,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE;oBACxC,QAAQ,EAAE,EAAE,wBAAwB,EAAE,GAAG,EAAE;iBAC5C;gBACD,iBAAiB,EAAE;oBACjB,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE;oBAChD,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE;iBACjC;gBACD,0BAA0B,EAAE,IAAI;gBAChC,sBAAsB,EAAE,GAAG;gBAC3B,mBAAmB,EAAE,IAAI;aAC1B;SACF,CAAC;QACF,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * ajv.ts — shared Ajv 2020 builder for the core schema tests.
3
+ *
4
+ * The three core schema tests (profile.test.ts, profile.schema.test.ts,
5
+ * profile.presets.test.ts) all need an identically-configured Ajv2020 instance
6
+ * with ajv-formats applied. This helper is the single source of that config so
7
+ * the `addFormats` interop cast lives in one place rather than being duplicated
8
+ * (and drifting) across three files.
9
+ *
10
+ * Test-only: Ajv + ajv-formats are dev dependencies. Importing this from
11
+ * production code would break the zero-runtime-dependencies invariant, so it
12
+ * lives under `test-helpers/` and is only ever imported by `*.test.ts` files.
13
+ */
14
+ import { Ajv2020 } from "ajv/dist/2020.js";
15
+ /**
16
+ * Build a fresh Ajv 2020 instance configured exactly as the core schema tests
17
+ * require: lenient strict mode (the schema uses draft-2020 features Ajv's strict
18
+ * mode flags as warnings), union types allowed, all errors collected, and
19
+ * ajv-formats applied so `pattern`/format keywords resolve.
20
+ */
21
+ export declare function buildAjv(): Ajv2020;
22
+ //# sourceMappingURL=ajv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ajv.d.ts","sourceRoot":"","sources":["../../src/test-helpers/ajv.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAO3C;;;;;GAKG;AACH,wBAAgB,QAAQ,IAAI,OAAO,CAQlC"}