tasknotes-spec 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.
- package/00-overview.md +172 -0
- package/01-terminology.md +156 -0
- package/02-model-and-mapping.md +288 -0
- package/03-temporal-semantics.md +290 -0
- package/04-recurrence.md +398 -0
- package/05-operations.md +968 -0
- package/06-validation.md +267 -0
- package/07-conformance.md +292 -0
- package/08-compatibility-and-migrations.md +188 -0
- package/09-configuration.md +837 -0
- package/10-dependencies-and-reminders.md +266 -0
- package/11-links.md +373 -0
- package/CHANGELOG.md +25 -0
- package/README.md +80 -0
- package/conformance/README.md +31 -0
- package/conformance/adapters/mdbase-tasknotes.adapter.mjs +141 -0
- package/conformance/adapters/tasknotes-core/conformance.ts +2498 -0
- package/conformance/adapters/tasknotes-core/create-compat.ts +1 -0
- package/conformance/adapters/tasknotes-core/date.ts +12 -0
- package/conformance/adapters/tasknotes-core/field-mapping.ts +14 -0
- package/conformance/adapters/tasknotes-core/recurrence.ts +10 -0
- package/conformance/adapters/tasknotes-date-bridge.ts +20 -0
- package/conformance/adapters/tasknotes-runtime-bridge.ts +1107 -0
- package/conformance/adapters/tasknotes-runtime-obsidian-shim.ts +84 -0
- package/conformance/adapters/tasknotes-templating-bridge.ts +13 -0
- package/conformance/adapters/tasknotes.adapter.mjs +485 -0
- package/conformance/docs/ADAPTER_CONTRACT.md +245 -0
- package/conformance/docs/FIXTURE_FORMAT.md +247 -0
- package/conformance/docs/RUNNER_GUIDE.md +393 -0
- package/conformance/fixtures/config-schema.json +634 -0
- package/conformance/fixtures/config.json +18984 -0
- package/conformance/fixtures/conformance.json +444 -0
- package/conformance/fixtures/create-compat.json +18639 -0
- package/conformance/fixtures/date.json +25612 -0
- package/conformance/fixtures/dependencies.json +8647 -0
- package/conformance/fixtures/field-mapping.json +5406 -0
- package/conformance/fixtures/links.json +1127 -0
- package/conformance/fixtures/migrations.json +668 -0
- package/conformance/fixtures/operations.json +2761 -0
- package/conformance/fixtures/recurrence.json +22958 -0
- package/conformance/fixtures/reminders.json +13333 -0
- package/conformance/fixtures/templating.json +497 -0
- package/conformance/fixtures/validation.json +5308 -0
- package/conformance/lib/adapter-loader.mjs +23 -0
- package/conformance/lib/load-fixtures.mjs +43 -0
- package/conformance/lib/matchers.mjs +200 -0
- package/conformance/manifest.json +232 -0
- package/conformance/scripts/generate-fixtures.mjs +5239 -0
- package/conformance/scripts/package-fixtures.mjs +101 -0
- package/conformance/tests/coverage.test.mjs +213 -0
- package/conformance/tests/runner.test.mjs +102 -0
- package/conformance/tests/tasknotes-runtime-routing.test.mjs +64 -0
- package/package.json +49 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Adapter Contract
|
|
2
|
+
|
|
3
|
+
Conformance fixtures are implementation-agnostic and live in `tasknotes-spec`. Each implementation provides an **adapter** — a callable component that bridges the conformance runner to the implementation under test.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Conceptual interface
|
|
8
|
+
|
|
9
|
+
An adapter is any callable that satisfies this interface:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
execute(operation: string, input: object) → { ok: bool, result?: object, error?: string, error_details?: object }
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And a metadata query:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
metadata() → {
|
|
19
|
+
implementation: string,
|
|
20
|
+
version: string,
|
|
21
|
+
spec_version: string,
|
|
22
|
+
validation_modes: string[],
|
|
23
|
+
profiles: string[],
|
|
24
|
+
capabilities: string[]
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### `execute`
|
|
29
|
+
|
|
30
|
+
- Accepts `operation` (a string operation name, e.g. `"date.parse_utc"`) and `input` (a plain object of arguments).
|
|
31
|
+
- Returns an **envelope** object:
|
|
32
|
+
- `ok` (boolean, required): `true` on success, `false` on failure
|
|
33
|
+
- `result` (object, optional): the operation's output, present when `ok` is `true`
|
|
34
|
+
- `error` (string, optional): a human-readable error message, present when `ok` is `false`
|
|
35
|
+
- `error_details` (object, optional): structured error fields (`operation`, `code`, `message`, optional `field/path`) when available
|
|
36
|
+
- Must never throw or panic for invalid inputs; all errors must be returned in the envelope.
|
|
37
|
+
|
|
38
|
+
### `metadata` / `meta.claim`
|
|
39
|
+
|
|
40
|
+
The runner calls the `meta.claim` operation (with `input = {}`) to obtain adapter metadata. The result object must contain:
|
|
41
|
+
|
|
42
|
+
| Field | Type | Description |
|
|
43
|
+
|-------|------|-------------|
|
|
44
|
+
| `implementation` | string | Name of the implementation under test |
|
|
45
|
+
| `version` | string | Version of the implementation |
|
|
46
|
+
| `spec_version` | string | `tasknotes-spec` version targeted by the adapter |
|
|
47
|
+
| `validation_modes` | string[] | Supported validation modes; must include `strict` |
|
|
48
|
+
| `profiles` | string[] | Conformance profiles claimed (e.g. `["core-lite", "recurrence"]`) |
|
|
49
|
+
| `capabilities` | string[] | Optional capability tokens claimed (e.g. `["dependencies", "links"]`) |
|
|
50
|
+
|
|
51
|
+
Profile-token consistency requirements:
|
|
52
|
+
|
|
53
|
+
- If `profiles` includes `extended`, `capabilities` must include `dependencies`, `reminders`, `links`, and `time-tracking`.
|
|
54
|
+
- If `profiles` includes `templating`, `capabilities` must include `templating`.
|
|
55
|
+
- If `profiles` includes `materialized-occurrences`, `profiles` must also include `recurrence` and `capabilities` must include `materialized-occurrences`.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Language bindings
|
|
60
|
+
|
|
61
|
+
### JavaScript binding
|
|
62
|
+
|
|
63
|
+
For JavaScript implementations, an adapter is an **ESM module** that the runner imports directly. It must export:
|
|
64
|
+
|
|
65
|
+
- `metadata` — an object (not a function) with the fields above
|
|
66
|
+
- `execute(operation, input)` — an async function returning an envelope
|
|
67
|
+
|
|
68
|
+
The runner locates the adapter module via the `TASKNOTES_ADAPTER` environment variable (an absolute or relative path to the `.mjs` file).
|
|
69
|
+
|
|
70
|
+
**Minimal example**:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
// my-adapter.mjs
|
|
74
|
+
export const metadata = {
|
|
75
|
+
implementation: "my-tasknotes",
|
|
76
|
+
version: "1.2.3",
|
|
77
|
+
spec_version: "0.2.0-draft",
|
|
78
|
+
validation_modes: ["strict"],
|
|
79
|
+
profiles: ["core-lite"],
|
|
80
|
+
capabilities: [],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export async function execute(operation, input) {
|
|
84
|
+
try {
|
|
85
|
+
const result = await myImplementation.run(operation, input);
|
|
86
|
+
return { ok: true, result };
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return { ok: false, error: err.message };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Run with:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
TASKNOTES_ADAPTER=./my-adapter.mjs npm run conformance:test
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Other language bindings
|
|
100
|
+
|
|
101
|
+
Any language may implement its own runner that calls the implementation directly, without the JS adapter module mechanism. The fixtures and this documentation are the normative source; the JS runner is the reference implementation.
|
|
102
|
+
|
|
103
|
+
A non-JS runner should:
|
|
104
|
+
|
|
105
|
+
1. Load the fixture JSON files from `fixtures/` (see `manifest.json` for the file list)
|
|
106
|
+
2. For each fixture, call the implementation's equivalent of `execute(operation, input)`
|
|
107
|
+
3. Apply the assertions as specified in `FIXTURE_FORMAT.md`
|
|
108
|
+
4. Skip fixtures whose `profile` is not satisfied by claimed profiles (including cumulative expansion rules from §7.3)
|
|
109
|
+
5. Skip fixtures whose `requires[]` capabilities are not claimed by the implementation
|
|
110
|
+
6. Report results (TAP output recommended; see `RUNNER_GUIDE.md`)
|
|
111
|
+
|
|
112
|
+
See `RUNNER_GUIDE.md` for a complete step-by-step guide to implementing a runner in any language.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Supported operations
|
|
117
|
+
|
|
118
|
+
All operation names that appear in the fixture files are listed here. Operations marked "(requires capability)" are only exercised when the adapter claims that capability via `meta.claim`.
|
|
119
|
+
|
|
120
|
+
### Core operations (no capability required)
|
|
121
|
+
|
|
122
|
+
- `date.parse_utc`
|
|
123
|
+
- `date.parse_local`
|
|
124
|
+
- `date.validate`
|
|
125
|
+
- `date.get_part`
|
|
126
|
+
- `date.has_time`
|
|
127
|
+
- `date.is_same`
|
|
128
|
+
- `date.is_before`
|
|
129
|
+
- `date.resolve_operation_target`
|
|
130
|
+
- `date.day_in_timezone`
|
|
131
|
+
- `field.default_mapping`
|
|
132
|
+
- `field.build_mapping`
|
|
133
|
+
- `field.normalize`
|
|
134
|
+
- `field.denormalize`
|
|
135
|
+
- `field.resolve_display_title`
|
|
136
|
+
- `field.is_completed_status`
|
|
137
|
+
- `field.default_completed_status`
|
|
138
|
+
- `recurrence.complete`
|
|
139
|
+
- `recurrence.recalculate`
|
|
140
|
+
- `recurrence.uncomplete_instance`
|
|
141
|
+
- `recurrence.skip_instance`
|
|
142
|
+
- `recurrence.unskip_instance`
|
|
143
|
+
- `recurrence.effective_state`
|
|
144
|
+
- `occurrence.materialize`
|
|
145
|
+
- `occurrence.complete`
|
|
146
|
+
- `occurrence.skip`
|
|
147
|
+
- `occurrence.uncomplete`
|
|
148
|
+
- `occurrence.unskip`
|
|
149
|
+
- `occurrence.unmaterialize`
|
|
150
|
+
- `create_compat.create`
|
|
151
|
+
- `meta.claim`
|
|
152
|
+
- `meta.has_capability`
|
|
153
|
+
- `meta.has_profile`
|
|
154
|
+
- `config.resolve_collection_path`
|
|
155
|
+
- `config.merge_top_level`
|
|
156
|
+
- `config.spec_version_effective`
|
|
157
|
+
- `config.map_tasknotes_plugin`
|
|
158
|
+
- `config.detect_task_file`
|
|
159
|
+
- `config.provider_behavior`
|
|
160
|
+
- `config.validate_schema`
|
|
161
|
+
- `validation.core_evaluate`
|
|
162
|
+
- `validation.time_entries`
|
|
163
|
+
- `op.mutate_with_validation`
|
|
164
|
+
- `op.atomic_write`
|
|
165
|
+
- `op.idempotency_check`
|
|
166
|
+
- `op.update_patch`
|
|
167
|
+
- `op.complete_nonrecurring`
|
|
168
|
+
- `op.uncomplete_nonrecurring`
|
|
169
|
+
- `op.error_shape`
|
|
170
|
+
- `delete.remove`
|
|
171
|
+
|
|
172
|
+
### Capability-gated operations
|
|
173
|
+
|
|
174
|
+
| Operation | Required capability |
|
|
175
|
+
|-----------|-------------------|
|
|
176
|
+
| `dependency.validate_entry` | `dependencies` |
|
|
177
|
+
| `dependency.validate_set` | `dependencies` |
|
|
178
|
+
| `dependency.add` | `dependencies` |
|
|
179
|
+
| `dependency.remove` | `dependencies` |
|
|
180
|
+
| `dependency.replace` | `dependencies` |
|
|
181
|
+
| `dependency.missing_target_behavior` | `dependencies` |
|
|
182
|
+
| `reminder.validate_entry` | `reminders` |
|
|
183
|
+
| `reminder.validate_set` | `reminders` |
|
|
184
|
+
| `reminder.add` | `reminders` |
|
|
185
|
+
| `reminder.update` | `reminders` |
|
|
186
|
+
| `reminder.remove` | `reminders` |
|
|
187
|
+
| `link.parse` | `links` |
|
|
188
|
+
| `link.resolve` | `links` |
|
|
189
|
+
| `link.update_references_on_rename` | `links` + `rename` |
|
|
190
|
+
| `occurrence.materialize` | `materialized-occurrences` |
|
|
191
|
+
| `occurrence.complete` | `materialized-occurrences` |
|
|
192
|
+
| `occurrence.skip` | `materialized-occurrences` |
|
|
193
|
+
| `occurrence.uncomplete` | `materialized-occurrences` |
|
|
194
|
+
| `occurrence.unskip` | `materialized-occurrences` |
|
|
195
|
+
| `occurrence.unmaterialize` | `materialized-occurrences` |
|
|
196
|
+
| `templating.expand_variables` | `templating` |
|
|
197
|
+
| `templating.merge_frontmatter` | `templating` |
|
|
198
|
+
| `templating.handle_failure` | `templating` |
|
|
199
|
+
| `templating.parse_sections` | `templating` |
|
|
200
|
+
| `templating.tokenize` | `templating` |
|
|
201
|
+
| `templating.create_pipeline` | `templating` |
|
|
202
|
+
| `templating.config_defaults` | `templating` |
|
|
203
|
+
| `templating.profile_claim_requirements` | `templating` |
|
|
204
|
+
| `migration.normalize_aliases` | `migration` |
|
|
205
|
+
| `migration.compat_mode` | `migration` |
|
|
206
|
+
| `migration.plan` | `migration` |
|
|
207
|
+
| `migration.normalize_temporal` | `migration` |
|
|
208
|
+
| `migration.resolve_instance_overlap` | `migration` |
|
|
209
|
+
| `migration.normalize_dependencies` | `migration` + `dependencies` |
|
|
210
|
+
| `migration.normalize_reminders` | `migration` + `reminders` |
|
|
211
|
+
| `migration.normalize_links` | `migration` + `links` |
|
|
212
|
+
| `migration.report_summary` | `migration` |
|
|
213
|
+
| `migration.divergence_register` | `migration` |
|
|
214
|
+
| `migration.deprecation_policy` | `migration` |
|
|
215
|
+
| `migration.safety_guards` | `migration` |
|
|
216
|
+
| `migration.compat_statement` | `migration` |
|
|
217
|
+
| `archive.apply` | `archive` |
|
|
218
|
+
| `rename.apply` | `rename` |
|
|
219
|
+
| `rename.title_storage_interaction` | `rename` |
|
|
220
|
+
| `batch.apply` | `batch` |
|
|
221
|
+
| `op.detect_conflict` | `concurrency` |
|
|
222
|
+
| `op.dry_run` | `dry-run` |
|
|
223
|
+
| `time.start` | `time-tracking` |
|
|
224
|
+
| `time.stop` | `time-tracking` |
|
|
225
|
+
| `time.replace_entries` | `time-tracking` |
|
|
226
|
+
| `time.remove_entry` | `time-tracking` |
|
|
227
|
+
| `time.auto_stop_on_complete` | `time-tracking` |
|
|
228
|
+
| `time.report_totals` | `time-tracking` |
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Fixture shape
|
|
233
|
+
|
|
234
|
+
Fixture files are JSON arrays. Each element is a fixture object with fields defined in `FIXTURE_FORMAT.md`.
|
|
235
|
+
|
|
236
|
+
Summary of fields:
|
|
237
|
+
|
|
238
|
+
- `id` (string, unique)
|
|
239
|
+
- `section` (string, spec reference)
|
|
240
|
+
- `profile` (string)
|
|
241
|
+
- `operation` (string)
|
|
242
|
+
- `assertion` (string)
|
|
243
|
+
- `requires` (string[], optional)
|
|
244
|
+
- `input` (object)
|
|
245
|
+
- `expect` (object, conditional)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Fixture Format
|
|
2
|
+
|
|
3
|
+
Each fixture is a JSON object:
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{
|
|
7
|
+
"id": "date.parse_utc.valid.0001",
|
|
8
|
+
"section": "§3",
|
|
9
|
+
"profile": "core-lite",
|
|
10
|
+
"operation": "date.parse_utc",
|
|
11
|
+
"assertion": "envelope_equals",
|
|
12
|
+
"requires": [],
|
|
13
|
+
"input": { "value": "2026-02-20" },
|
|
14
|
+
"expect": {
|
|
15
|
+
"ok": true,
|
|
16
|
+
"result": { "date": "2026-02-20" }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Fields
|
|
22
|
+
|
|
23
|
+
| Field | Type | Required | Description |
|
|
24
|
+
|-------|------|----------|-------------|
|
|
25
|
+
| `id` | string | yes | Unique fixture identifier, e.g. `date.parse_utc.valid.0001` |
|
|
26
|
+
| `section` | string | yes | Spec section this fixture exercises, e.g. `§3` |
|
|
27
|
+
| `profile` | string | yes | Conformance profile: `core-lite`, `recurrence`, `extended`, `templating`, or `materialized-occurrences`; runner skips when adapter claim does not satisfy profile (including cumulative expansion rules) |
|
|
28
|
+
| `operation` | string | yes | Operation name passed to the adapter, e.g. `date.parse_utc` |
|
|
29
|
+
| `assertion` | string | yes | Assertion kind; see below |
|
|
30
|
+
| `requires` | string[] | no | Capability tokens required; after profile check, runner skips if the adapter does not claim all listed capabilities |
|
|
31
|
+
| `input` | object | yes | Arguments passed to the operation |
|
|
32
|
+
| `expect` | object | conditional | Expected outcome; required for `envelope_equals` and `envelope_error` |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Assertion kinds
|
|
37
|
+
|
|
38
|
+
### `envelope_equals`
|
|
39
|
+
|
|
40
|
+
Recursively deep-matches the adapter's response envelope against `expect` using the matcher directives described below. Every key in `expect` must be present and matching in the actual envelope; extra keys in the actual envelope are allowed.
|
|
41
|
+
|
|
42
|
+
**Inputs**: `input` (passed to operation), `expect` (partial envelope to match)
|
|
43
|
+
|
|
44
|
+
**Passes when**: `deepMatch(actualEnvelope, expect)` succeeds without error.
|
|
45
|
+
|
|
46
|
+
**Example**:
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"assertion": "envelope_equals",
|
|
50
|
+
"input": { "value": "2026-02-20" },
|
|
51
|
+
"expect": { "ok": true, "result": { "date": "2026-02-20" } }
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### `envelope_error`
|
|
58
|
+
|
|
59
|
+
Asserts that the operation returns a failure envelope, and optionally matches the error message.
|
|
60
|
+
|
|
61
|
+
**Passes when**:
|
|
62
|
+
1. `envelope.ok === false`
|
|
63
|
+
2. If `expect.error` is present: `deepMatch(envelope.error, expect.error)` succeeds
|
|
64
|
+
|
|
65
|
+
**Example**:
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"assertion": "envelope_error",
|
|
69
|
+
"input": { "value": "not-a-date" },
|
|
70
|
+
"expect": { "error": { "$regex": "invalid" } }
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
If `expect` is absent or `expect.error` is absent, only `ok === false` is checked.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### `recurrence_complete_invariants`
|
|
79
|
+
|
|
80
|
+
Asserts structural and semantic invariants on the result of a `recurrence.complete` operation. Does **not** use `expect`; instead applies fixed logical checks derived from `input`.
|
|
81
|
+
|
|
82
|
+
**Passes when ALL of the following hold**:
|
|
83
|
+
|
|
84
|
+
1. `envelope.ok === true`
|
|
85
|
+
2. `result.completeInstances` is an array
|
|
86
|
+
3. `result.skippedInstances` is an array
|
|
87
|
+
4. `result.completeInstances` **includes** `input.completionDate`
|
|
88
|
+
5. `result.skippedInstances` does **not** include `input.completionDate`
|
|
89
|
+
6. `result.updatedRecurrence` matches `/FREQ=/`
|
|
90
|
+
7. `result.updatedRecurrence` matches `/DTSTART:/`
|
|
91
|
+
8. **Anchor-conditional DTSTART day** (if `input.recurrenceAnchor === "completion"`):
|
|
92
|
+
- Extract `dtstartDay` = `input.completionDate` with hyphens removed (e.g. `"2026-03-01"` → `"20260301"`)
|
|
93
|
+
- `result.updatedRecurrence` matches `/DTSTART:20260301(?:;|$)/`
|
|
94
|
+
9. **Scheduled anchor** (if `input.recurrenceAnchor === "scheduled"` and `input.scheduled` is a string):
|
|
95
|
+
- Extract `dtstartDay` = first 10 chars of `input.scheduled` with hyphens removed
|
|
96
|
+
- `result.updatedRecurrence` matches `/DTSTART:{dtstartDay}(?:;|$)/`
|
|
97
|
+
10. If `result.nextScheduled` is present:
|
|
98
|
+
- It matches `/^\d{4}-\d{2}-\d{2}/`
|
|
99
|
+
- `result.nextScheduled.slice(0, 10) >= input.completionDate` (string comparison)
|
|
100
|
+
11. If `result.nextScheduled`, `result.nextDue`, `input.scheduled`, and `input.due` are all present strings:
|
|
101
|
+
- `originalOffset` = `(Date(input.due[0:10]) - Date(input.scheduled[0:10]))` in whole days
|
|
102
|
+
- `(Date(result.nextDue[0:10]) - Date(result.nextScheduled[0:10]))` in whole days === `originalOffset`
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### `recurrence_recalculate_invariants`
|
|
107
|
+
|
|
108
|
+
Asserts structural and semantic invariants on the result of a `recurrence.recalculate` operation. Does **not** use `expect`.
|
|
109
|
+
|
|
110
|
+
**Passes when ALL of the following hold**:
|
|
111
|
+
|
|
112
|
+
1. `envelope.ok === true`
|
|
113
|
+
2. `result.updatedRecurrence` matches `/FREQ=/`
|
|
114
|
+
3. If `input.recurrenceAnchor === "scheduled"`:
|
|
115
|
+
- `result.updatedRecurrence` matches `/DTSTART:/`
|
|
116
|
+
4. If `result.nextScheduled` is present:
|
|
117
|
+
- `result.nextScheduled.slice(0, 10) >= input.referenceDate` (string comparison)
|
|
118
|
+
- Let `complete = input.completeInstances ?? []` and `skipped = input.skippedInstances ?? []`
|
|
119
|
+
- If `input.recurrenceAnchor !== "completion"`: `complete` does **not** include `result.nextScheduled.slice(0, 10)`
|
|
120
|
+
- `skipped` does **not** include `result.nextScheduled.slice(0, 10)`
|
|
121
|
+
5. If `result.nextScheduled`, `result.nextDue`, `input.scheduled`, and `input.due` are all present strings:
|
|
122
|
+
- `originalOffset` = `(Date(input.due[0:10]) - Date(input.scheduled[0:10]))` in whole days
|
|
123
|
+
- `(Date(result.nextDue[0:10]) - Date(result.nextScheduled[0:10]))` in whole days === `originalOffset`
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### `create_compat_invariants`
|
|
128
|
+
|
|
129
|
+
Asserts the result of a `create_compat.create` operation. Combines `envelope_equals` matching with additional path invariants.
|
|
130
|
+
|
|
131
|
+
**Passes when ALL of the following hold**:
|
|
132
|
+
|
|
133
|
+
1. `deepMatch(actualEnvelope, expect)` succeeds (same as `envelope_equals`)
|
|
134
|
+
2. If `envelope.ok === true` and `envelope.result.path` is present:
|
|
135
|
+
- `envelope.result.path` ends with `.md`
|
|
136
|
+
- `envelope.result.path` does **not** contain `{`
|
|
137
|
+
- `envelope.result.path` does **not** contain `}`
|
|
138
|
+
|
|
139
|
+
The path checks ensure template variables were fully expanded and no literal brace characters remain.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Matcher directives in `expect`
|
|
144
|
+
|
|
145
|
+
Matcher directives are special single-key objects that appear within `expect` values. When the deep-matcher encounters an object with exactly one of these keys, it applies the corresponding matching logic instead of a structural equality check.
|
|
146
|
+
|
|
147
|
+
Directives may appear at any level of nesting. The matching context (the full `input` object) is threaded through all recursive calls to support `$ref`.
|
|
148
|
+
|
|
149
|
+
### `$regex`
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{ "$regex": "^\\d{4}-\\d{2}-\\d{2}$" }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Asserts the actual value is a **string** matching the given regular expression. The pattern is interpreted as a standard ECMAScript regex (compatible with most language regex engines).
|
|
156
|
+
|
|
157
|
+
**Algorithm**:
|
|
158
|
+
```
|
|
159
|
+
if actual is not a string → fail
|
|
160
|
+
compile $regex as a regex pattern
|
|
161
|
+
if actual does not match pattern → fail
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### `$contains`
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{ "$contains": ["item1", "item2"] }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
or for objects:
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{ "$contains": { "key": "value" } }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**For arrays**: asserts the actual array contains at least one element matching each item in the `$contains` array (order-independent). Each element is matched with full `deepMatch`.
|
|
179
|
+
|
|
180
|
+
**For objects**: asserts the actual object contains all key-value pairs in the `$contains` object. Values are matched with full `deepMatch`.
|
|
181
|
+
|
|
182
|
+
**Algorithm (array)**:
|
|
183
|
+
```
|
|
184
|
+
if actual is not an array → fail
|
|
185
|
+
for each expectedItem in $contains:
|
|
186
|
+
if no element in actual deepMatches expectedItem → fail
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Algorithm (object)**:
|
|
190
|
+
```
|
|
191
|
+
if actual is not an object → fail
|
|
192
|
+
for each (key, value) in $contains:
|
|
193
|
+
deepMatch(actual[key], value)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
### `$oneOf`
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{ "$oneOf": ["OPEN", "IN_PROGRESS", null] }
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Asserts the actual value matches **at least one** of the listed alternatives. Each alternative is matched with full `deepMatch` (so alternatives may themselves contain directives).
|
|
205
|
+
|
|
206
|
+
**Algorithm**:
|
|
207
|
+
```
|
|
208
|
+
for each option in $oneOf:
|
|
209
|
+
try deepMatch(actual, option)
|
|
210
|
+
if it succeeds → pass
|
|
211
|
+
fail (no option matched)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
### `$ref`
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{ "$ref": "input.completionDate" }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Resolves a dotted path into the fixture's `input` object and uses the resolved value as the expected value. The path must start with `"input."`.
|
|
223
|
+
|
|
224
|
+
**Algorithm**:
|
|
225
|
+
```
|
|
226
|
+
if ref does not start with "input." → treat as literal value (no resolution)
|
|
227
|
+
segments = ref["input.".length:].split(".")
|
|
228
|
+
current = context.input
|
|
229
|
+
for each segment in segments:
|
|
230
|
+
if current is null or not an object → resolved = undefined; break
|
|
231
|
+
current = current[segment]
|
|
232
|
+
resolved = current
|
|
233
|
+
deepMatch(actual, resolved)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Example**: if `input = { "completionDate": "2026-03-01" }` and the expect field is `{ "$ref": "input.completionDate" }`, the matcher checks `actual === "2026-03-01"`.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Reference implementation
|
|
241
|
+
|
|
242
|
+
The canonical implementation of all matchers and assertion kinds is:
|
|
243
|
+
|
|
244
|
+
- **Matchers**: `conformance/lib/matchers.mjs` — `applyAssertion(caseDef, envelope)` and internal `deepMatch`
|
|
245
|
+
- **Runner**: `conformance/tests/runner.test.mjs` — loads fixtures, calls adapter, applies assertions
|
|
246
|
+
|
|
247
|
+
When this documentation and the reference implementation disagree, file an issue; the intent is for them to be identical.
|