bantamkit-mcp 0.26.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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/assets/contracts/default.yaml +90 -0
  4. package/assets/evals/devteam/manifest.yaml +351 -0
  5. package/assets/evals/devteam/repo/HISTORY.md +18 -0
  6. package/assets/evals/devteam/repo/README.md +12 -0
  7. package/assets/evals/devteam/repo/docs/architecture.md +17 -0
  8. package/assets/evals/devteam/repo/docs/runbook.md +10 -0
  9. package/assets/evals/devteam/repo/issues/142-settlement-timeout.md +23 -0
  10. package/assets/evals/devteam/repo/patches/0009-retry-budget.patch +38 -0
  11. package/assets/evals/devteam/repo/src/ledger/__init__.py +3 -0
  12. package/assets/evals/devteam/repo/src/ledger/config.py +35 -0
  13. package/assets/evals/devteam/repo/src/ledger/errors.py +13 -0
  14. package/assets/evals/devteam/repo/src/ledger/posting.py +12 -0
  15. package/assets/evals/devteam/repo/src/ledger/registry.py +7 -0
  16. package/assets/evals/devteam/repo/src/ledger/report.py +9 -0
  17. package/assets/evals/devteam/repo/src/ledger/retry.py +17 -0
  18. package/assets/evals/devteam/repo/src/ledger/settle.py +16 -0
  19. package/assets/evals/devteam/repo/src/ledger/validate.py +14 -0
  20. package/assets/evals/devteam/repo/tests/test_posting.py +13 -0
  21. package/assets/evals/devteam/repo/tests/test_settle.py +9 -0
  22. package/assets/evals/devteam/tasks/dt-error-contract.yaml +186 -0
  23. package/assets/evals/devteam/tasks/dt-handler-map.yaml +183 -0
  24. package/assets/evals/devteam/tasks/dt-patch-before-after.yaml +182 -0
  25. package/assets/evals/devteam/tasks/dt-retry-attempts.yaml +181 -0
  26. package/assets/evals/devteam/tasks/dt-settlement-config.yaml +185 -0
  27. package/assets/evals/devteam/tasks/dt-symbol-home.yaml +181 -0
  28. package/assets/evals/devteam/tasks/dt-trace-blame.yaml +182 -0
  29. package/assets/evals/devteam/tasks/dt-unread-key.yaml +180 -0
  30. package/assets/evals/document/tasks/doc-large-in-137.yaml +38 -0
  31. package/assets/evals/document/tasks/doc-large-in-359.yaml +44 -0
  32. package/assets/evals/document/tasks/doc-large-in-372.yaml +38 -0
  33. package/assets/evals/document/tasks/doc-large-out-11764.yaml +37 -0
  34. package/assets/evals/document/tasks/doc-large-out-4137.yaml +37 -0
  35. package/assets/evals/document/tasks/doc-large-out-8022.yaml +37 -0
  36. package/assets/evals/document/tasks/doc-small-137.yaml +37 -0
  37. package/assets/evals/document/tasks/doc-small-261.yaml +37 -0
  38. package/assets/evals/document/tasks/doc-small-388.yaml +37 -0
  39. package/assets/evals/fixtures/.gitkeep +0 -0
  40. package/assets/evals/fixtures/catalog.json +6 -0
  41. package/assets/evals/perturbations/task-completion.yaml +576 -0
  42. package/assets/evals/tasks/.gitkeep +0 -0
  43. package/assets/evals/tasks/extract-contact.yaml +14 -0
  44. package/assets/evals/tasks/extract-invoice.yaml +14 -0
  45. package/assets/evals/tasks/extract-order.yaml +15 -0
  46. package/assets/evals/tasks/extract-schedule.yaml +14 -0
  47. package/assets/evals/tasks/extract-versions.yaml +17 -0
  48. package/assets/evals/tasks/nav-prod-port.yaml +84 -0
  49. package/assets/evals/tasks/nav-release-bundle.yaml +87 -0
  50. package/assets/evals/tasks/recall-audit-retention.yaml +17 -0
  51. package/assets/evals/tasks/recall-cache-ttl.yaml +13 -0
  52. package/assets/evals/tasks/recall-db-port.yaml +17 -0
  53. package/assets/evals/tasks/recall-deploy.yaml +13 -0
  54. package/assets/evals/tasks/recall-env-endpoint.yaml +18 -0
  55. package/assets/evals/tasks/recall-oncall-rotation.yaml +21 -0
  56. package/assets/evals/tasks/recall-oncall.yaml +13 -0
  57. package/assets/evals/tasks/recall-org-quota.yaml +18 -0
  58. package/assets/evals/tasks/recall-owner.yaml +13 -0
  59. package/assets/evals/tasks/shop-basket-total.yaml +10 -0
  60. package/assets/evals/tasks/shop-cheapest.yaml +9 -0
  61. package/assets/evals/tasks/shop-compare.yaml +9 -0
  62. package/assets/evals/tasks/shop-gadget-value.yaml +9 -0
  63. package/assets/evals/tasks/shop-stock-total.yaml +9 -0
  64. package/assets/evals/tasks/shop-total.yaml +9 -0
  65. package/assets/profiles/default.yaml +31 -0
  66. package/assets/profiles/patient.yaml +31 -0
  67. package/assets/rubrics/.gitkeep +0 -0
  68. package/assets/rubrics/code-quality.yaml +20 -0
  69. package/assets/rubrics/grounded-completion.yaml +37 -0
  70. package/assets/rubrics/task-completion.yaml +28 -0
  71. package/assets/schemas/shiftwork-checkpoint.json +188 -0
  72. package/assets/skills/.gitkeep +0 -0
  73. package/assets/skills/file-graph.md +7 -0
  74. package/assets/skills/memory.md +35 -0
  75. package/assets/tools/.gitkeep +0 -0
  76. package/assets/tools/bantamkit_read.json +48 -0
  77. package/assets/tools/bantamkit_status.json +25 -0
  78. package/assets/tools/build_identity.json +17 -0
  79. package/assets/tools/document_list.json +12 -0
  80. package/assets/tools/document_read.json +31 -0
  81. package/assets/tools/file_graph.json +12 -0
  82. package/assets/tools/memory_compact.json +31 -0
  83. package/assets/tools/memory_recall.json +38 -0
  84. package/assets/tools/memory_save.json +61 -0
  85. package/assets/tools/shiftwork_clock_in.json +25 -0
  86. package/assets/tools/shiftwork_clock_out.json +60 -0
  87. package/assets/tools/shiftwork_status.json +25 -0
  88. package/assets/tools/skill_audit.json +70 -0
  89. package/assets/tools/validate_json.json +31 -0
  90. package/dist/assets.d.ts +48 -0
  91. package/dist/assets.js +109 -0
  92. package/dist/charsets.d.ts +17 -0
  93. package/dist/charsets.js +580 -0
  94. package/dist/cli.d.ts +12 -0
  95. package/dist/cli.js +280 -0
  96. package/dist/contract.d.ts +145 -0
  97. package/dist/contract.js +608 -0
  98. package/dist/docread.d.ts +357 -0
  99. package/dist/docread.js +3315 -0
  100. package/dist/errors.d.ts +11 -0
  101. package/dist/errors.js +14 -0
  102. package/dist/eventlog.d.ts +137 -0
  103. package/dist/eventlog.js +294 -0
  104. package/dist/htmlentities.d.ts +2 -0
  105. package/dist/htmlentities.js +16 -0
  106. package/dist/index.d.ts +23 -0
  107. package/dist/index.js +13 -0
  108. package/dist/mcp/identity.d.ts +25 -0
  109. package/dist/mcp/identity.js +275 -0
  110. package/dist/mcp/pyargs.d.ts +80 -0
  111. package/dist/mcp/pyargs.js +362 -0
  112. package/dist/mcp/sdkjson.d.ts +56 -0
  113. package/dist/mcp/sdkjson.js +91 -0
  114. package/dist/mcp/server.d.ts +45 -0
  115. package/dist/mcp/server.js +740 -0
  116. package/dist/mcp/status.d.ts +210 -0
  117. package/dist/mcp/status.js +357 -0
  118. package/dist/mcp/transport.d.ts +68 -0
  119. package/dist/mcp/transport.js +111 -0
  120. package/dist/mcpreport.d.ts +214 -0
  121. package/dist/mcpreport.js +758 -0
  122. package/dist/memory/cli.d.ts +2 -0
  123. package/dist/memory/cli.js +396 -0
  124. package/dist/memory/component.d.ts +256 -0
  125. package/dist/memory/component.js +497 -0
  126. package/dist/memory/factfile.d.ts +120 -0
  127. package/dist/memory/factfile.js +406 -0
  128. package/dist/memory/layers.d.ts +92 -0
  129. package/dist/memory/layers.js +410 -0
  130. package/dist/memory/pyfs.d.ts +521 -0
  131. package/dist/memory/pyfs.js +1608 -0
  132. package/dist/memory/pyyaml.d.ts +155 -0
  133. package/dist/memory/pyyaml.js +868 -0
  134. package/dist/memory/store.d.ts +478 -0
  135. package/dist/memory/store.js +1034 -0
  136. package/dist/pyargparse.d.ts +141 -0
  137. package/dist/pyargparse.js +813 -0
  138. package/dist/pyjson.d.ts +110 -0
  139. package/dist/pyjson.js +524 -0
  140. package/dist/pyjsonschema.d.ts +50 -0
  141. package/dist/pyjsonschema.js +951 -0
  142. package/dist/shiftwork.d.ts +60 -0
  143. package/dist/shiftwork.js +324 -0
  144. package/dist/skillaudit.d.ts +172 -0
  145. package/dist/skillaudit.js +914 -0
  146. package/dist/statusline.d.ts +106 -0
  147. package/dist/statusline.js +281 -0
  148. package/package.json +45 -0
@@ -0,0 +1,868 @@
1
+ /**
2
+ * PyYAML's `safe_dump` — the parts of it a six-key fact-file frontmatter reaches, ported
3
+ * arm for arm from PyYAML 6.0.3's `emitter.py` and `resolver.py`.
4
+ *
5
+ * WHY THIS FILE EXISTS INSTEAD OF `js-yaml`
6
+ * -----------------------------------------
7
+ * `_write_fact` in `runtime-py/src/bantamkit/memory/store.py` writes
8
+ *
9
+ * "---\n" + yaml.safe_dump(meta, sort_keys=False, allow_unicode=True) + "---\n\n" + ...
10
+ *
11
+ * and `_stamp` rewrites a fact file on EVERY recall hit, so a Node server re-emits
12
+ * Python's files continuously from day one. 35 of the 65 files in the live store carry
13
+ * PyYAML's 80-column plain-scalar wrap. Measured on a copy of that store: js-yaml 5.3.0
14
+ * emits a `>-` folded block where PyYAML emits a wrapped plain scalar, `lineWidth:-1`
15
+ * removes the wrap rather than reproducing it, and `noArrayIndent:true` is a no-op. No
16
+ * option combination matches. The failure would raise nothing — the file still parses to
17
+ * the same string — the store would just silently stop being diffable against Python's.
18
+ *
19
+ * So this is not a YAML emitter. It is PyYAML's emitter for one document shape: a
20
+ * root-level block mapping whose values are strings, `null`, or lists of strings.
21
+ * Everything outside that shape throws rather than guessing.
22
+ *
23
+ * COLUMNS ARE CODEPOINTS. PyYAML does `self.column += len(data)` on a `str`, which counts
24
+ * codepoints. `String.length` in JS counts UTF-16 units and charges 2 for an astral
25
+ * emoji, which moves the wrap. Every scalar here is handled as an array of codepoints for
26
+ * exactly that reason.
27
+ */
28
+ import { pyFloatRepr } from './pyfs.js';
29
+ export const STR_TAG = 'tag:yaml.org,2002:str';
30
+ export const NULL_TAG = 'tag:yaml.org,2002:null';
31
+ const BOOL_TAG = 'tag:yaml.org,2002:bool';
32
+ const INT_TAG = 'tag:yaml.org,2002:int';
33
+ const FLOAT_TAG = 'tag:yaml.org,2002:float';
34
+ const MERGE_TAG = 'tag:yaml.org,2002:merge';
35
+ export const TIMESTAMP_TAG = 'tag:yaml.org,2002:timestamp';
36
+ const VALUE_TAG = 'tag:yaml.org,2002:value';
37
+ const YAML_TAG = 'tag:yaml.org,2002:yaml';
38
+ export class YamlEmitError extends Error {
39
+ }
40
+ // --------------------------------------------------------------------------- resolver
41
+ /**
42
+ * `SafeResolver.yaml_implicit_resolvers`, transcribed from the installed PyYAML.
43
+ *
44
+ * The order inside a bucket is the registration order and it is load-bearing — PyYAML
45
+ * returns the FIRST match, so `bool` beats `null` for the `n`/`N` bucket. The bucket keys
46
+ * are first characters; `''` is the bucket the empty string uses, which is why `""`
47
+ * resolves to null and therefore cannot be written plain.
48
+ */
49
+ const RESOLVERS = [
50
+ [
51
+ BOOL_TAG,
52
+ /^(?:yes|Yes|YES|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)$/,
53
+ ['y', 'Y', 'n', 'N', 't', 'T', 'f', 'F', 'o', 'O'],
54
+ ],
55
+ [
56
+ FLOAT_TAG,
57
+ /^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)?|\.[0-9][0-9_]*(?:[eE][-+][0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*|[-+]?\.(?:inf|Inf|INF)|\.(?:nan|NaN|NAN))$/,
58
+ ['-', '+', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.'],
59
+ ],
60
+ [
61
+ INT_TAG,
62
+ /^(?:[-+]?0b[0-1_]+|[-+]?0[0-7_]+|[-+]?(?:0|[1-9][0-9_]*)|[-+]?0x[0-9a-fA-F_]+|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$/,
63
+ ['-', '+', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
64
+ ],
65
+ [MERGE_TAG, /^(?:<<)$/, ['<']],
66
+ [NULL_TAG, /^(?:~|null|Null|NULL|)$/, ['~', 'n', 'N', '']],
67
+ [
68
+ TIMESTAMP_TAG,
69
+ /^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]|[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?(?:[Tt]|[ \t]+)[0-9][0-9]?:[0-9][0-9]:[0-9][0-9](?:\.[0-9]*)?(?:[ \t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$/,
70
+ ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
71
+ ],
72
+ [VALUE_TAG, /^(?:=)$/, ['=']],
73
+ [YAML_TAG, /^(?:!|&|\*)$/, ['!', '&', '*']],
74
+ ];
75
+ /**
76
+ * The tag PyYAML's resolver gives the PLAIN spelling of `value`.
77
+ *
78
+ * A string may be written plain only when this answers `str` — otherwise reading the file
79
+ * back gives a different type. That is the whole reason `created: '2026-08-23'` is
80
+ * quoted: bare, it resolves to `timestamp` and comes back as a `datetime.date`.
81
+ */
82
+ export function resolveImplicitTag(value) {
83
+ const first = value === '' ? '' : String.fromCodePoint(value.codePointAt(0));
84
+ for (const [tag, pattern, firsts] of RESOLVERS) {
85
+ if (firsts.includes(first) && pattern.test(value)) {
86
+ return tag;
87
+ }
88
+ }
89
+ return STR_TAG;
90
+ }
91
+ const BREAKS = '\n\x85\u2028\u2029';
92
+ const WS_OR_BREAK = '\0 \t\r\n\x85\u2028\u2029';
93
+ const isBreak = (ch) => BREAKS.includes(ch);
94
+ /** `Emitter.analyze_scalar`, with `allow_unicode=True` fixed (that is what `_write_fact` passes). */
95
+ function analyzeScalar(chars) {
96
+ if (chars.length === 0) {
97
+ return {
98
+ empty: true,
99
+ multiline: false,
100
+ allowFlowPlain: false,
101
+ allowBlockPlain: true,
102
+ allowSingleQuoted: true,
103
+ allowDoubleQuoted: true,
104
+ allowBlock: false,
105
+ };
106
+ }
107
+ const scalar = chars.join('');
108
+ let blockIndicators = false;
109
+ let flowIndicators = false;
110
+ let lineBreaks = false;
111
+ let specialCharacters = false;
112
+ let leadingSpace = false;
113
+ let leadingBreak = false;
114
+ let trailingSpace = false;
115
+ let trailingBreak = false;
116
+ let breakSpace = false;
117
+ let spaceBreak = false;
118
+ if (scalar.startsWith('---') || scalar.startsWith('...')) {
119
+ blockIndicators = true;
120
+ flowIndicators = true;
121
+ }
122
+ let precededByWhitespace = true;
123
+ let followedByWhitespace = chars.length === 1 || WS_OR_BREAK.includes(chars[1]);
124
+ let previousSpace = false;
125
+ let previousBreak = false;
126
+ let index = 0;
127
+ while (index < chars.length) {
128
+ const ch = chars[index];
129
+ if (index === 0) {
130
+ if ('#,[]{}&*!|>\'"%@`'.includes(ch)) {
131
+ flowIndicators = true;
132
+ blockIndicators = true;
133
+ }
134
+ if (ch === '?' || ch === ':') {
135
+ flowIndicators = true;
136
+ if (followedByWhitespace)
137
+ blockIndicators = true;
138
+ }
139
+ if (ch === '-' && followedByWhitespace) {
140
+ flowIndicators = true;
141
+ blockIndicators = true;
142
+ }
143
+ }
144
+ else {
145
+ if (',?[]{}'.includes(ch))
146
+ flowIndicators = true;
147
+ if (ch === ':') {
148
+ flowIndicators = true;
149
+ if (followedByWhitespace)
150
+ blockIndicators = true;
151
+ }
152
+ if (ch === '#' && precededByWhitespace) {
153
+ flowIndicators = true;
154
+ blockIndicators = true;
155
+ }
156
+ }
157
+ if (isBreak(ch))
158
+ lineBreaks = true;
159
+ const cp = ch.codePointAt(0);
160
+ if (!(ch === '\n' || (cp >= 0x20 && cp <= 0x7e))) {
161
+ // `allow_unicode=True`, so anything printable-by-YAML's-definition is fine as-is
162
+ // and only genuine control/surrogate/noncharacter codepoints force double quotes.
163
+ const printable = (cp === 0x85 || (cp >= 0xa0 && cp <= 0xd7ff) || (cp >= 0xe000 && cp <= 0xfffd) ||
164
+ (cp >= 0x10000 && cp < 0x10ffff)) &&
165
+ cp !== 0xfeff;
166
+ if (!printable)
167
+ specialCharacters = true;
168
+ }
169
+ if (ch === ' ') {
170
+ if (index === 0)
171
+ leadingSpace = true;
172
+ if (index === chars.length - 1)
173
+ trailingSpace = true;
174
+ if (previousBreak)
175
+ breakSpace = true;
176
+ previousSpace = true;
177
+ previousBreak = false;
178
+ }
179
+ else if (isBreak(ch)) {
180
+ if (index === 0)
181
+ leadingBreak = true;
182
+ if (index === chars.length - 1)
183
+ trailingBreak = true;
184
+ if (previousSpace)
185
+ spaceBreak = true;
186
+ previousSpace = false;
187
+ previousBreak = true;
188
+ }
189
+ else {
190
+ previousSpace = false;
191
+ previousBreak = false;
192
+ }
193
+ index += 1;
194
+ precededByWhitespace = WS_OR_BREAK.includes(ch);
195
+ followedByWhitespace =
196
+ index + 1 >= chars.length || WS_OR_BREAK.includes(chars[index + 1]);
197
+ }
198
+ let allowFlowPlain = true;
199
+ let allowBlockPlain = true;
200
+ let allowSingleQuoted = true;
201
+ const allowDoubleQuoted = true;
202
+ let allowBlock = true;
203
+ if (leadingSpace || leadingBreak || trailingSpace || trailingBreak) {
204
+ allowFlowPlain = allowBlockPlain = false;
205
+ }
206
+ if (trailingSpace)
207
+ allowBlock = false;
208
+ if (breakSpace)
209
+ allowFlowPlain = allowBlockPlain = allowSingleQuoted = false;
210
+ if (spaceBreak || specialCharacters) {
211
+ allowFlowPlain = allowBlockPlain = allowSingleQuoted = allowBlock = false;
212
+ }
213
+ if (lineBreaks)
214
+ allowFlowPlain = allowBlockPlain = false;
215
+ if (flowIndicators)
216
+ allowFlowPlain = false;
217
+ if (blockIndicators)
218
+ allowBlockPlain = false;
219
+ return {
220
+ empty: false,
221
+ multiline: lineBreaks,
222
+ allowFlowPlain,
223
+ allowBlockPlain,
224
+ allowSingleQuoted,
225
+ allowDoubleQuoted,
226
+ allowBlock,
227
+ };
228
+ }
229
+ // ---------------------------------------------------------------------------- writers
230
+ const ESCAPE_REPLACEMENTS = new Map([
231
+ [0x00, '0'],
232
+ [0x07, 'a'],
233
+ [0x08, 'b'],
234
+ [0x09, 't'],
235
+ [0x0a, 'n'],
236
+ [0x0b, 'v'],
237
+ [0x0c, 'f'],
238
+ [0x0d, 'r'],
239
+ [0x1b, 'e'],
240
+ [0x22, '"'],
241
+ [0x5c, '\\'],
242
+ [0x85, 'N'],
243
+ [0xa0, '_'],
244
+ [0x2028, 'L'],
245
+ [0x2029, 'P'],
246
+ ]);
247
+ const hex = (cp, width) => cp.toString(16).toUpperCase().padStart(width, '0');
248
+ /**
249
+ * PyYAML's `Emitter`, reduced to the state four writers actually read: `column`,
250
+ * `whitespace`, `indention`, `indent`. `best_width` is 80 and `best_indent` is 2 because
251
+ * `safe_dump` passes neither `width` nor `indent`.
252
+ */
253
+ class Emitter {
254
+ parts = [];
255
+ column = 0;
256
+ whitespace = true;
257
+ indention = true;
258
+ indent = null;
259
+ indents = [];
260
+ bestWidth = 80;
261
+ bestIndent = 2;
262
+ toString() {
263
+ return this.parts.join('');
264
+ }
265
+ push(data) {
266
+ this.parts.push(data);
267
+ this.column += [...data].length;
268
+ }
269
+ increaseIndent(flow, indentless) {
270
+ this.indents.push(this.indent);
271
+ if (this.indent === null) {
272
+ this.indent = flow ? this.bestIndent : 0;
273
+ }
274
+ else if (!indentless) {
275
+ this.indent += this.bestIndent;
276
+ }
277
+ }
278
+ popIndent() {
279
+ this.indent = this.indents.pop() ?? null;
280
+ }
281
+ get atIndention() {
282
+ return this.indention;
283
+ }
284
+ writeIndicator(indicator, needWhitespace, whitespace = false, indention = false) {
285
+ const data = this.whitespace || !needWhitespace ? indicator : ' ' + indicator;
286
+ this.whitespace = whitespace;
287
+ this.indention = this.indention && indention;
288
+ this.push(data);
289
+ }
290
+ writeLineBreak(data = '\n') {
291
+ this.whitespace = true;
292
+ this.indention = true;
293
+ this.parts.push(data);
294
+ this.column = 0;
295
+ }
296
+ writeIndent() {
297
+ const indent = this.indent ?? 0;
298
+ if (!this.indention || this.column > indent || (this.column === indent && !this.whitespace)) {
299
+ this.writeLineBreak();
300
+ }
301
+ if (this.column < indent) {
302
+ this.whitespace = true;
303
+ this.parts.push(' '.repeat(indent - this.column));
304
+ this.column = indent;
305
+ }
306
+ }
307
+ /** `Emitter.process_scalar` for the three styles a `style=None` scalar can take. */
308
+ writeScalar(value, tag, simpleKeyContext) {
309
+ const chars = [...value];
310
+ const analysis = analyzeScalar(chars);
311
+ const implicit = resolveImplicitTag(value) === tag;
312
+ const split = !simpleKeyContext;
313
+ // `choose_scalar_style`, with `flow_level` fixed at 0: this document shape puts no
314
+ // scalar inside a flow collection (the only flow collection it emits is `[]`).
315
+ let style = '"';
316
+ if (implicit && !(simpleKeyContext && (analysis.empty || analysis.multiline)) && analysis.allowBlockPlain) {
317
+ style = '';
318
+ }
319
+ else if (analysis.allowSingleQuoted && !(simpleKeyContext && analysis.multiline)) {
320
+ style = "'";
321
+ }
322
+ if (style === '')
323
+ this.writePlain(chars, split);
324
+ else if (style === "'")
325
+ this.writeSingleQuoted(chars, split);
326
+ else
327
+ this.writeDoubleQuoted(chars, split);
328
+ }
329
+ /**
330
+ * `write_plain`. The wrap: at a run of exactly one space, if the column is ALREADY past
331
+ * 80 — the check runs after the word before the space has been written — the space
332
+ * becomes a line break plus the current indent. A run of two or more spaces is written
333
+ * literally and never split, and a scalar with no space in it never wraps at all.
334
+ */
335
+ writePlain(chars, split) {
336
+ if (chars.length === 0)
337
+ return;
338
+ if (!this.whitespace)
339
+ this.push(' ');
340
+ this.whitespace = false;
341
+ this.indention = false;
342
+ let spaces = false;
343
+ let breaks = false;
344
+ let start = 0;
345
+ let end = 0;
346
+ while (end <= chars.length) {
347
+ const ch = end < chars.length ? chars[end] : null;
348
+ if (spaces) {
349
+ if (ch !== ' ') {
350
+ if (start + 1 === end && this.column > this.bestWidth && split) {
351
+ this.writeIndent();
352
+ this.whitespace = false;
353
+ this.indention = false;
354
+ }
355
+ else {
356
+ this.push(chars.slice(start, end).join(''));
357
+ }
358
+ start = end;
359
+ }
360
+ }
361
+ else if (breaks) {
362
+ // Unreachable for a plain scalar: `line_breaks` clears `allow_block_plain`, so a
363
+ // multiline value is never written plain. Kept because Python keeps it.
364
+ if (ch === null || !isBreak(ch)) {
365
+ if (chars[start] === '\n')
366
+ this.writeLineBreak();
367
+ for (const br of chars.slice(start, end))
368
+ this.writeLineBreak(br);
369
+ this.writeIndent();
370
+ this.whitespace = false;
371
+ this.indention = false;
372
+ start = end;
373
+ }
374
+ }
375
+ else {
376
+ if (ch === null || ch === ' ' || isBreak(ch)) {
377
+ this.push(chars.slice(start, end).join(''));
378
+ start = end;
379
+ }
380
+ }
381
+ if (ch !== null) {
382
+ spaces = ch === ' ';
383
+ breaks = isBreak(ch);
384
+ }
385
+ end += 1;
386
+ }
387
+ }
388
+ /**
389
+ * `write_single_quoted`. Same wrap as `write_plain` plus two guards Python only has
390
+ * here — `start != 0 and end != len(text)`, which is why a leading or trailing space
391
+ * survives a wrap instead of being eaten — and `''` for an embedded quote. Its `breaks`
392
+ * arm IS reachable: a description containing `\n` is single-quoted, not double-quoted,
393
+ * because a line break clears only `allow_block_plain`.
394
+ */
395
+ writeSingleQuoted(chars, split) {
396
+ this.writeIndicator("'", true);
397
+ let spaces = false;
398
+ let breaks = false;
399
+ let start = 0;
400
+ let end = 0;
401
+ while (end <= chars.length) {
402
+ const ch = end < chars.length ? chars[end] : null;
403
+ if (spaces) {
404
+ if (ch === null || ch !== ' ') {
405
+ if (start + 1 === end && this.column > this.bestWidth && split && start !== 0 && end !== chars.length) {
406
+ this.writeIndent();
407
+ }
408
+ else {
409
+ this.push(chars.slice(start, end).join(''));
410
+ }
411
+ start = end;
412
+ }
413
+ }
414
+ else if (breaks) {
415
+ if (ch === null || !isBreak(ch)) {
416
+ if (chars[start] === '\n')
417
+ this.writeLineBreak();
418
+ for (const br of chars.slice(start, end))
419
+ this.writeLineBreak(br);
420
+ this.writeIndent();
421
+ start = end;
422
+ }
423
+ }
424
+ else {
425
+ if (ch === null || ch === ' ' || isBreak(ch) || ch === "'") {
426
+ if (start < end) {
427
+ this.push(chars.slice(start, end).join(''));
428
+ start = end;
429
+ }
430
+ }
431
+ }
432
+ if (ch === "'") {
433
+ this.push("''");
434
+ start = end + 1;
435
+ }
436
+ if (ch !== null) {
437
+ spaces = ch === ' ';
438
+ breaks = isBreak(ch);
439
+ }
440
+ end += 1;
441
+ }
442
+ this.writeIndicator("'", false);
443
+ }
444
+ /** `write_double_quoted`, with `allow_unicode=True`. */
445
+ writeDoubleQuoted(chars, split) {
446
+ this.writeIndicator('"', true);
447
+ let start = 0;
448
+ let end = 0;
449
+ while (end <= chars.length) {
450
+ const ch = end < chars.length ? chars[end] : null;
451
+ const cp = ch === null ? -1 : ch.codePointAt(0);
452
+ const mustEscape = ch === null ||
453
+ '"\\\x85\u2028\u2029\ufeff'.includes(ch) ||
454
+ !((cp >= 0x20 && cp <= 0x7e) || (cp >= 0xa0 && cp <= 0xd7ff) || (cp >= 0xe000 && cp <= 0xfffd));
455
+ if (mustEscape) {
456
+ if (start < end) {
457
+ this.push(chars.slice(start, end).join(''));
458
+ start = end;
459
+ }
460
+ if (ch !== null) {
461
+ let data;
462
+ const rep = ESCAPE_REPLACEMENTS.get(cp);
463
+ if (rep !== undefined)
464
+ data = '\\' + rep;
465
+ else if (cp <= 0xff)
466
+ data = '\\x' + hex(cp, 2);
467
+ else if (cp <= 0xffff)
468
+ data = '\\u' + hex(cp, 4);
469
+ else
470
+ data = '\\U' + hex(cp, 8);
471
+ this.push(data);
472
+ start = end + 1;
473
+ }
474
+ }
475
+ if (0 < end &&
476
+ end < chars.length - 1 &&
477
+ (ch === ' ' || start >= end) &&
478
+ this.column + (end - start) > this.bestWidth &&
479
+ split) {
480
+ const data = chars.slice(start, end).join('') + '\\';
481
+ if (start < end)
482
+ start = end;
483
+ this.push(data);
484
+ this.writeIndent();
485
+ this.whitespace = false;
486
+ this.indention = false;
487
+ if (chars[start] === ' ') {
488
+ this.push('\\');
489
+ }
490
+ }
491
+ end += 1;
492
+ }
493
+ this.writeIndicator('"', false);
494
+ }
495
+ }
496
+ /**
497
+ * `yaml.safe_dump(mapping, sort_keys=False, allow_unicode=True)` for a root block mapping.
498
+ *
499
+ * `entries` is an ordered array rather than an object because the ORDER is the contract —
500
+ * `sort_keys=False` means the emitted order is the insertion order of `_write_fact`'s
501
+ * dict literal, and an object here would invite someone to reorder it without noticing.
502
+ */
503
+ export function safeDumpMapping(entries) {
504
+ const e = new Emitter();
505
+ e.increaseIndent(false, false); // expect_block_mapping: indent None -> 0
506
+ for (const [key, value] of entries) {
507
+ e.writeIndent();
508
+ e.writeScalar(key, STR_TAG, true);
509
+ e.writeIndicator(':', false);
510
+ if (value === null) {
511
+ // `represent_none` emits the scalar `null` under the null tag, which resolves back
512
+ // to null and is therefore written plain.
513
+ e.increaseIndent(false, false);
514
+ e.writeScalar('null', NULL_TAG, false);
515
+ e.popIndent();
516
+ }
517
+ else if (Array.isArray(value)) {
518
+ if (value.length === 0) {
519
+ // `check_empty_sequence()` sends an empty sequence down the FLOW path even under
520
+ // `default_flow_style=False`, which is why this one collection is `[]`.
521
+ e.writeIndicator('[', true, true);
522
+ e.writeIndicator(']', false);
523
+ }
524
+ else {
525
+ // `expect_block_sequence`: `indentless = mapping_context and not indention`, and
526
+ // writing the `:` cleared `indention`, so the dashes sit at column 0.
527
+ e.increaseIndent(false, !e.atIndention);
528
+ for (const item of value) {
529
+ e.writeIndent();
530
+ e.writeIndicator('-', true, false, true);
531
+ e.increaseIndent(false, false);
532
+ // `links:\n- ~` puts a `None` in the list, and `represent_none` writes `null`.
533
+ if (item === null)
534
+ e.writeScalar('null', NULL_TAG, false);
535
+ else if (item instanceof PyScalar)
536
+ e.writeScalar(item.yaml, item.tag, false);
537
+ else
538
+ e.writeScalar(item, STR_TAG, false);
539
+ e.popIndent();
540
+ }
541
+ e.popIndent();
542
+ }
543
+ }
544
+ else if (typeof value === 'string') {
545
+ e.increaseIndent(false, false);
546
+ e.writeScalar(value, STR_TAG, false);
547
+ e.popIndent();
548
+ }
549
+ else if (value instanceof PyScalar) {
550
+ // The node carries its own tag, so `choose_scalar_style` finds the implicit spelling
551
+ // and writes it PLAIN — `2026-08-23`, `true`, `31`, `.inf` — exactly as PyYAML does
552
+ // for the object `safe_load` handed back.
553
+ e.increaseIndent(false, false);
554
+ e.writeScalar(value.yaml, value.tag, false);
555
+ e.popIndent();
556
+ }
557
+ else {
558
+ throw new YamlEmitError(`cannot emit ${typeof value} for key ${key}`);
559
+ }
560
+ }
561
+ e.popIndent(); // the MappingEndEvent pop, before expect_document_end's write_indent
562
+ e.writeIndent();
563
+ return e.toString();
564
+ }
565
+ // ------------------------------------------------------------------- the constructor half
566
+ /**
567
+ * `SafeConstructor`, for the tags a fact-file frontmatter can resolve to — the READ side of
568
+ * the resolver above.
569
+ *
570
+ * WHY THIS EXISTS. `_facts` in `runtime-py` does no type check: it puts `yaml.safe_load`'s
571
+ * answer straight into the `Fact` dataclass, so `description: 2026` gives Python an `int`
572
+ * and `created: 2026-08-23` gives it a `datetime.date`, and the store then interpolates
573
+ * those into `index.md`, into the file NAME on disk, into the token text the duplicate gate
574
+ * scores, and back through `yaml.safe_dump` on the next `_stamp`. Measured over 26 shapes:
575
+ * CPython raises on FOUR of them and answers on the other 22. This port used to raise on
576
+ * ALL of them, and one hand-edited fact file therefore took `memory_recall`, `memory_save`
577
+ * and the index rebuild down for a whole store the Python server was still serving.
578
+ *
579
+ * THE RULING — WHAT A `Fact` FIELD HOLDS. A `string` when PyYAML resolves `str`, `null` when
580
+ * it resolves `null`, and otherwise a `PyScalar`: a frozen record of the three things the
581
+ * store can observe about the value — how `str()` spells it, how `safe_dump` re-emits it,
582
+ * and whether it is truthy. Not a `Date`, not a `number`, not a `bigint` at the field. The
583
+ * reason is that the store never does arithmetic on these; it renders them, at nine places,
584
+ * and two of the renderings DIFFER (`True` against `true`, `inf` against `.inf`). A field
585
+ * holding a JS `number` would have to re-derive both at every one of those nine, which is
586
+ * how the index line and the file on disk come to spell one fact two ways.
587
+ */
588
+ export class PyScalar {
589
+ /** The resolved YAML tag, which is also the tag the emitter writes it back under. */
590
+ tag;
591
+ /** `type(value).__name__` — what `list()` and a failed `<` put in a `TypeError`. */
592
+ pyType;
593
+ /** `str(value)`: what an f-string interpolates. */
594
+ text;
595
+ /** `safe_dump`'s plain scalar: what `_write_fact` puts back on disk. */
596
+ yaml;
597
+ /** `bool(value)`: `created or _mtime_date(path)` and `links or []` both read it. */
598
+ truthy;
599
+ /** What `<` orders on. `recall`'s tie-break is the only reader; see `store.pyCompareLt`. */
600
+ ord;
601
+ constructor(tag, pyType, text, yaml, truthy, ord) {
602
+ this.tag = tag;
603
+ this.pyType = pyType;
604
+ this.text = text;
605
+ this.yaml = yaml;
606
+ this.truthy = truthy;
607
+ this.ord = ord;
608
+ Object.freeze(this);
609
+ }
610
+ /** So a bare template literal spells it Python's way; `pyText` is the explicit route. */
611
+ toString() {
612
+ return this.text;
613
+ }
614
+ }
615
+ /**
616
+ * A tag `SafeConstructor` has no constructor for, or a value its constructor refuses.
617
+ *
618
+ * Both are `_facts`-catchable in the reference — `ConstructorError` is a `YAMLError` and
619
+ * `datetime`'s complaints are `ValueError` — so both come out as `malformed fact file
620
+ * <name>: <reason>`. The REASONS differ in the two cases and this port only claims the
621
+ * second: see the ruling on `constructPlain`.
622
+ */
623
+ export class YamlConstructError extends Error {
624
+ }
625
+ /** `days_from_civil`: days since 1970-01-01 for a proleptic Gregorian date. */
626
+ function daysFromCivil(y, m, d) {
627
+ const yy = y - (m <= 2 ? 1 : 0);
628
+ const era = Math.floor(yy / 400);
629
+ const yoe = yy - era * 400;
630
+ const doy = Math.floor((153 * (m + (m > 2 ? -3 : 9)) + 2) / 5) + d - 1;
631
+ const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy;
632
+ return era * 146097 + doe - 719468;
633
+ }
634
+ const BOOL_VALUES = {
635
+ yes: true,
636
+ no: false,
637
+ true: true,
638
+ false: false,
639
+ on: true,
640
+ off: false,
641
+ };
642
+ /**
643
+ * `SafeConstructor.construct_yaml_int`, which is NOT `parseInt`.
644
+ *
645
+ * `_` is dropped everywhere first, the sign is peeled off, and then the base is chosen by
646
+ * PREFIX: `0b`, `0x`, a bare leading `0` (OCTAL — `017` is 15, not 17), a `:` (sexagesimal,
647
+ * base 60 from the right), and decimal otherwise. `bigint` because Python's `int` does not
648
+ * overflow and a 24-digit hand-edit must round-trip through `str()` exactly.
649
+ */
650
+ function constructInt(raw) {
651
+ let value = raw.replace(/_/g, '');
652
+ let sign = 1n;
653
+ if (value[0] === '-')
654
+ sign = -1n;
655
+ if (value[0] === '+' || value[0] === '-')
656
+ value = value.slice(1);
657
+ if (value === '0')
658
+ return 0n;
659
+ if (value.startsWith('0b'))
660
+ return sign * BigInt(`0b${value.slice(2)}`);
661
+ if (value.startsWith('0x'))
662
+ return sign * BigInt(`0x${value.slice(2)}`);
663
+ if (value[0] === '0')
664
+ return sign * BigInt(`0o${value.slice(1)}`);
665
+ if (value.includes(':')) {
666
+ let total = 0n;
667
+ let base = 1n;
668
+ for (const part of value.split(':').reverse()) {
669
+ total += BigInt(part) * base;
670
+ base *= 60n;
671
+ }
672
+ return sign * total;
673
+ }
674
+ return sign * BigInt(value);
675
+ }
676
+ /** `SafeConstructor.construct_yaml_float`. Same shape as the int one, plus `.inf`/`.nan`. */
677
+ function constructFloat(raw) {
678
+ let value = raw.replace(/_/g, '').toLowerCase();
679
+ let sign = 1;
680
+ if (value[0] === '-')
681
+ sign = -1;
682
+ if (value[0] === '+' || value[0] === '-')
683
+ value = value.slice(1);
684
+ if (value === '.inf')
685
+ return sign * Infinity;
686
+ if (value === '.nan')
687
+ return NaN;
688
+ if (value.includes(':')) {
689
+ let total = 0;
690
+ let base = 1;
691
+ for (const part of value.split(':').reverse()) {
692
+ total += Number(part) * base;
693
+ base *= 60;
694
+ }
695
+ return sign * total;
696
+ }
697
+ return sign * Number(value);
698
+ }
699
+ /** `SafeRepresenter.represent_float`, which is not `repr` — `.inf`, `.nan`, and a forced `.0`. */
700
+ function representFloat(x) {
701
+ if (Number.isNaN(x))
702
+ return '.nan';
703
+ if (x === Infinity)
704
+ return '.inf';
705
+ if (x === -Infinity)
706
+ return '-.inf';
707
+ const value = pyFloatRepr(x).toLowerCase();
708
+ // PyYAML's own comment: `repr(1e17)` has no decimal point and `1e+17` is not a `!!float`
709
+ // by the tag's definition, so it splices one in before the exponent.
710
+ return !value.includes('.') && value.includes('e') ? value.replace('e', '.0e') : value;
711
+ }
712
+ /** `SafeConstructor.timestamp_regexp`, which is WIDER than the resolver's pattern. */
713
+ const TIMESTAMP_RE = /^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:(?:[Tt]|[ \t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\.([0-9]*))?(?:[ \t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?)?$/;
714
+ const pad = (n, w) => String(n).padStart(w, '0');
715
+ const LEAP = (y) => (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
716
+ const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
717
+ /**
718
+ * `repr(datetime.timedelta)`, for the ONE sentence that interpolates one.
719
+ *
720
+ * `datetime.timezone(delta)` refuses an offset of 24 hours or more and prints the offending
721
+ * `timedelta` with `%r`. `timedelta` normalises so that `0 <= seconds < 86400` with the sign
722
+ * carried by `days`, and its repr omits every field that is zero — `timedelta(days=1)`, not
723
+ * `timedelta(days=1, seconds=0)`. A `+24:00` tz in a hand-edited `created` reaches it.
724
+ */
725
+ function reprTimedelta(totalSeconds) {
726
+ const days = Math.floor(totalSeconds / 86400);
727
+ const seconds = totalSeconds - days * 86400;
728
+ const parts = [];
729
+ if (days !== 0)
730
+ parts.push(`days=${days}`);
731
+ if (seconds !== 0)
732
+ parts.push(`seconds=${seconds}`);
733
+ if (parts.length === 0)
734
+ parts.push('0');
735
+ return `datetime.timedelta(${parts.join(', ')})`;
736
+ }
737
+ /**
738
+ * `SafeConstructor.construct_yaml_timestamp`, plus the `datetime` constructor's own refusals.
739
+ *
740
+ * The refusals are the reason this is spelled out rather than handed to `Date`: `Date` has
741
+ * no year 0 and silently ROLLS OVER a month of 13 into the next January, where CPython
742
+ * raises `ValueError('month must be in 1..12')` — and that string is what `_facts` puts in
743
+ * front of a model. `Date` also cannot hold a year-1 timestamp in every zone. Nothing here
744
+ * needs an instant; both renderings are calendar text.
745
+ */
746
+ function constructTimestamp(raw) {
747
+ const m = TIMESTAMP_RE.exec(raw);
748
+ if (m === null) {
749
+ // Unreachable from the resolver, whose pattern is a subset of this one. Kept because a
750
+ // silent `null` here would be a wrong Fact rather than a refusal.
751
+ throw new YamlConstructError(`${JSON.stringify(raw)} resolved to a timestamp but does not parse as one`);
752
+ }
753
+ const [, y, mo, d, h, mi, s, frac, tz, tzSign, tzHour, tzMinute] = m;
754
+ const year = Number(y);
755
+ const month = Number(mo);
756
+ const day = Number(d);
757
+ if (year < 1)
758
+ throw new YamlConstructError(`year ${year} is out of range`);
759
+ if (month < 1 || month > 12)
760
+ throw new YamlConstructError('month must be in 1..12');
761
+ const maxDay = month === 2 && LEAP(year) ? 29 : MONTH_DAYS[month - 1];
762
+ if (day < 1 || day > maxDay)
763
+ throw new YamlConstructError('day is out of range for month');
764
+ const date = `${pad(year, 4)}-${pad(month, 2)}-${pad(day, 2)}`;
765
+ if (h === undefined) {
766
+ // `datetime.date`: `str()` and `isoformat()` are the same text, so one string serves
767
+ // the f-string and the emitter both, and the ISO spelling sorts chronologically.
768
+ return new PyScalar(TIMESTAMP_TAG, 'datetime.date', date, date, true, { kind: 'date', iso: date });
769
+ }
770
+ const hour = Number(h);
771
+ const minute = Number(mi);
772
+ const second = Number(s);
773
+ if (hour > 23)
774
+ throw new YamlConstructError('hour must be in 0..23');
775
+ if (minute > 59)
776
+ throw new YamlConstructError('minute must be in 0..59');
777
+ if (second > 59)
778
+ throw new YamlConstructError('second must be in 0..59');
779
+ // `fraction` is truncated to six digits and right-padded to six: PyYAML's own arithmetic,
780
+ // and `datetime` has microsecond resolution and no more.
781
+ const micro = frac ? Number(frac.slice(0, 6).padEnd(6, '0')) : 0;
782
+ let offset = '';
783
+ let offsetSeconds = 0;
784
+ let aware = false;
785
+ if (tzSign !== undefined) {
786
+ const total = (Number(tzHour) * 3600 + Number(tzMinute ?? 0) * 60) * (tzSign === '-' ? -1 : 1);
787
+ if (Math.abs(total) >= 86400) {
788
+ throw new YamlConstructError('offset must be a timedelta strictly between -timedelta(hours=24) and ' +
789
+ `timedelta(hours=24), not ${reprTimedelta(total)}.`);
790
+ }
791
+ const sign = total < 0 ? '-' : '+';
792
+ const abs = Math.abs(total);
793
+ offset = `${sign}${pad(Math.floor(abs / 3600), 2)}:${pad(Math.floor(abs / 60) % 60, 2)}`;
794
+ offsetSeconds = total;
795
+ aware = true;
796
+ }
797
+ else if (tz !== undefined) {
798
+ offset = '+00:00'; // `Z` is `timezone.utc`, and `isoformat` spells that `+00:00`
799
+ aware = true;
800
+ }
801
+ // `datetime.isoformat(' ')` IS `str(datetime)`, so again one string serves both readers.
802
+ const text = `${date} ${pad(hour, 2)}:${pad(minute, 2)}:${pad(second, 2)}` +
803
+ `${micro ? `.${pad(micro, 6)}` : ''}${offset}`;
804
+ // An aware datetime orders by its UTC instant; a naive one by its wall clock. Python
805
+ // refuses to compare the two, which `pyCompareLt` reproduces off `aware`.
806
+ const sec = daysFromCivil(year, month, day) * 86400 + hour * 3600 + minute * 60 + second - offsetSeconds;
807
+ return new PyScalar(TIMESTAMP_TAG, 'datetime.datetime', text, text, true, {
808
+ kind: 'datetime',
809
+ aware,
810
+ sec,
811
+ us: micro,
812
+ });
813
+ }
814
+ /**
815
+ * What `yaml.safe_load` builds from ONE plain scalar in a fact-file frontmatter.
816
+ *
817
+ * RULING — THE WIDTH OF THIS, measured over 26 shapes against the reference and not
818
+ * generalised from any of them:
819
+ *
820
+ * - `str` and `null` come back as a JS `string` and `null`. Unchanged.
821
+ * - `bool`, `int`, `float` and `timestamp` come back as a `PyScalar` carrying `str()`,
822
+ * `safe_dump`'s spelling and `bool()`. 22 of the 26 shapes land here, including the
823
+ * ten N8 named, and each is compared at all nine render points in the `store` suite.
824
+ * - `timestamp` whose fields are out of range RAISES with CPython's own `ValueError`
825
+ * text (`month must be in 1..12`, `day is out of range for month`,
826
+ * `hour must be in 0..23`, `minute must be in 0..59`, `second must be in 0..59`,
827
+ * `year N is out of range`, and the `timedelta` sentence for a tz past 24 hours).
828
+ * Matched exactly, because `_facts` catches `ValueError` and interpolates it.
829
+ * - `merge` (`<<`) and `value` (`=`) RAISE, as CPython does, and the SENTENCE DIFFERS:
830
+ * PyYAML's `ConstructorError` carries the source marks and a rendered snippet. That is
831
+ * the same surface the module already refuses to port for `ScannerError`, and it is
832
+ * carried as a must-differ case rather than claimed.
833
+ * - `yaml` (`!`, `&`, `*`) RAISES, and CPython does NOT agree for all three: `k: !` is a
834
+ * tag PROPERTY on an empty node in the real scanner and answers `None`, while `&` and
835
+ * `*` are an anchor and an alias and answer with a `ScannerError`. This codec has no
836
+ * scanner and sees three plain scalars. All three are must-differ cases; `!` is the one
837
+ * where CPython answers and this port refuses, and it is written down as that rather
838
+ * than filed with the other two.
839
+ */
840
+ export function constructPlain(value) {
841
+ const tag = resolveImplicitTag(value);
842
+ switch (tag) {
843
+ case STR_TAG:
844
+ return value;
845
+ case NULL_TAG:
846
+ return null;
847
+ case BOOL_TAG: {
848
+ const b = BOOL_VALUES[value.toLowerCase()];
849
+ // `bool` IS an `int` in Python: `True < 7` is True and `True == 1`.
850
+ return new PyScalar(tag, 'bool', b ? 'True' : 'False', b ? 'true' : 'false', b, {
851
+ kind: 'num',
852
+ n: b ? 1n : 0n,
853
+ });
854
+ }
855
+ case INT_TAG: {
856
+ const n = constructInt(value);
857
+ return new PyScalar(tag, 'int', n.toString(), n.toString(), n !== 0n, { kind: 'num', n });
858
+ }
859
+ case FLOAT_TAG: {
860
+ const f = constructFloat(value);
861
+ return new PyScalar(tag, 'float', pyFloatRepr(f), representFloat(f), f !== 0, { kind: 'num', n: f });
862
+ }
863
+ case TIMESTAMP_TAG:
864
+ return constructTimestamp(value);
865
+ default:
866
+ throw new YamlConstructError(`could not determine a constructor for the tag '${tag}'`);
867
+ }
868
+ }