@crossworks/content-core 0.230.43

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 (44) hide show
  1. package/LICENSE.md +135 -0
  2. package/package.json +41 -0
  3. package/src/block-diff.test.ts +190 -0
  4. package/src/block-diff.ts +163 -0
  5. package/src/block-ids.test.ts +358 -0
  6. package/src/block-ids.ts +242 -0
  7. package/src/block-list.test.ts +241 -0
  8. package/src/block-list.ts +177 -0
  9. package/src/contacts-format.ts +260 -0
  10. package/src/doc-to-markdown.test.ts +194 -0
  11. package/src/doc-to-markdown.ts +315 -0
  12. package/src/formula-dimensions.test.ts +103 -0
  13. package/src/formula-dimensions.ts +231 -0
  14. package/src/formula-eval.ts +294 -0
  15. package/src/formula-seed.test.ts +175 -0
  16. package/src/formula-seed.ts +466 -0
  17. package/src/formula-signature.test.ts +336 -0
  18. package/src/formula-signature.ts +435 -0
  19. package/src/formula-spec.test.ts +458 -0
  20. package/src/formula-spec.ts +566 -0
  21. package/src/journal-options.test.ts +57 -0
  22. package/src/journal-options.ts +77 -0
  23. package/src/markdown-refs.test.ts +143 -0
  24. package/src/markdown-refs.ts +172 -0
  25. package/src/markdown-to-doc.test.ts +179 -0
  26. package/src/markdown-to-doc.ts +567 -0
  27. package/src/onboarding-questions.test.ts +75 -0
  28. package/src/onboarding-questions.ts +90 -0
  29. package/src/page-diff.test.ts +82 -0
  30. package/src/page-diff.ts +120 -0
  31. package/src/page-split.test.ts +141 -0
  32. package/src/page-split.ts +128 -0
  33. package/src/page-toc.test.ts +58 -0
  34. package/src/page-toc.ts +89 -0
  35. package/src/persona-bank.test.ts +67 -0
  36. package/src/persona-bank.ts +234 -0
  37. package/src/table-formula-mathjs.ts +259 -0
  38. package/src/table-formula.test.ts +157 -0
  39. package/src/table-formula.ts +496 -0
  40. package/src/table-model.test.ts +429 -0
  41. package/src/table-model.ts +870 -0
  42. package/src/thinking-tiers.ts +56 -0
  43. package/tsconfig.json +4 -0
  44. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,336 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parseFormulaSpec, type FormulaSpec } from './formula-spec';
3
+ import { evaluateSpec } from './formula-eval';
4
+ import { signatureForTarget, signatureLine, signatureOf } from './formula-signature';
5
+
6
+ /**
7
+ * Same model the spec tests are held to — the release-quantity calculation from
8
+ * API RP 581 Part 3 — because the signature's whole job is to predict what
9
+ * `evaluateSpec` will demand of it, and that is the spec with every construct
10
+ * in play at once.
11
+ */
12
+ const raw = {
13
+ id: 'api581-release-quantity',
14
+ name: 'Release Quantity',
15
+ source: { standard: 'API RP 581', part: '3', sections: ['5.3.2'] },
16
+ unitSystem: 'USC',
17
+ variables: [
18
+ { symbol: 'Cd', role: 'constant', value: 0.61, unit: null },
19
+ { symbol: 'C1', role: 'constant', value: 12, unit: 'in/ft' },
20
+ { symbol: 'C2', role: 'constant', value: 1, unit: null },
21
+ { symbol: 'gc', role: 'constant', value: 32.2, unit: 'lbm-ft/(lbf-s2)' },
22
+ { symbol: 'R', role: 'constant', value: 1545, unit: 'ft-lbf/(lb-mol·°R)' },
23
+ { symbol: 'k', role: 'constant', value: 1.5, unit: null },
24
+ { symbol: 'Patm', role: 'constant', value: 14.7, unit: 'lbf/in2 (abs)' },
25
+ { symbol: 'd', role: 'input', value: 0.375, unit: 'in' },
26
+ { symbol: 'An', role: 'derived', expression: 'PI / 4 * {d} ^ 2', unit: 'in2' },
27
+ { symbol: 'rho_l', role: 'input', unit: 'lb/ft3' },
28
+ { symbol: 'Pgauge', role: 'input', unit: 'lbf/in2 (g)' },
29
+ { symbol: 'Ps', role: 'input', unit: 'lbf/in2 (abs)' },
30
+ { symbol: 'MW', role: 'input', unit: 'lb/lb-mol' },
31
+ { symbol: 'Ts', role: 'input', unit: 'R' },
32
+ { symbol: 'Wn', role: 'output', unit: 'lb/sec' },
33
+ ],
34
+ expressions: [
35
+ {
36
+ id: 'liquid-release-rate',
37
+ equation: '3.3',
38
+ resultSymbol: 'Wn',
39
+ unit: 'lb/sec',
40
+ expression: '{Cd} * {rho_l} * ({An} / {C1}) * SQRT(2 * {gc} * {Pgauge} / {rho_l})',
41
+ },
42
+ {
43
+ id: 'transition-pressure',
44
+ equation: '3.7',
45
+ unverified: 'Supplied from memory; the source never defines it.',
46
+ resultSymbol: 'Ptrans',
47
+ expression: '{Patm} * (({k} + 1) / 2) ^ ({k} / ({k} - 1))',
48
+ },
49
+ {
50
+ id: 'vapor-sonic',
51
+ equation: '3.6',
52
+ expression: '({Cd} / {C2}) * {An} * {Ps} * SQRT( ({k} * {MW} * {gc}) / ({R} * {Ts}) )',
53
+ },
54
+ {
55
+ id: 'vapor-subsonic',
56
+ equation: '3.5',
57
+ expression: '({Cd} / {C2}) * {An} * {Ps} * SQRT( ({MW} * {gc}) / ({R} * {Ts}) )',
58
+ },
59
+ ],
60
+ piecewise: [
61
+ {
62
+ id: 'vapor-release-rate',
63
+ resultSymbol: 'Wn',
64
+ cases: [
65
+ { when: '{Ps} > {Ptrans}', use: 'vapor-sonic', label: 'Sonic' },
66
+ { when: '{Ps} <= {Ptrans}', use: 'vapor-subsonic', label: 'Subsonic' },
67
+ ],
68
+ },
69
+ ],
70
+ lookups: [
71
+ {
72
+ id: 'fact_di',
73
+ name: 'Release Magnitude Reduction Factor',
74
+ keys: ['detection', 'isolation'],
75
+ result: 'fact_di',
76
+ domains: { detection: ['A', 'B', 'C'], isolation: ['A', 'B', 'C'] },
77
+ rows: [
78
+ { detection: 'A', isolation: 'A', fact_di: 0.25 },
79
+ { detection: 'A', isolation: 'B', fact_di: 0.2 },
80
+ { detection: 'B', isolation: 'C', fact_di: 0.1 },
81
+ { detection: 'C', isolation: 'C', fact_di: 0.0 },
82
+ ],
83
+ },
84
+ {
85
+ id: 'ld_max',
86
+ name: 'Maximum Leak Duration',
87
+ keys: ['detection', 'holeSize'],
88
+ result: 'ld_max',
89
+ rows: [
90
+ { detection: 'A', holeSize: '1/4 in', ld_max: 20 },
91
+ { detection: 'A', holeSize: '1 in', ld_max: 10 },
92
+ { detection: 'B', holeSize: '1/4 in', ld_max: 60 },
93
+ ],
94
+ },
95
+ ],
96
+ classifications: [
97
+ {
98
+ id: 'detection-rating',
99
+ domain: ['A', 'B', 'C'],
100
+ criteria: {
101
+ A: 'Instrumentation designed specifically to detect material losses.',
102
+ B: 'Suitably located detectors outside the pressure-containing envelope.',
103
+ C: 'Visual detection, cameras, or detectors with marginal coverage.',
104
+ },
105
+ },
106
+ ],
107
+ };
108
+
109
+ function parse(input: unknown): FormulaSpec {
110
+ const result = parseFormulaSpec(input);
111
+ if (!result.ok) throw new Error(`fixture failed to parse: ${result.errors.join('; ')}`);
112
+ return result.spec;
113
+ }
114
+
115
+ const spec = parse(raw);
116
+ const sigOf = (id: string) => {
117
+ const found = signatureForTarget(spec, id);
118
+ if (!found) throw new Error(`no signature for '${id}'`);
119
+ return found;
120
+ };
121
+ const symbols = (id: string) => sigOf(id).inputs.map((i) => i.symbol);
122
+ const required = (id: string) =>
123
+ sigOf(id)
124
+ .inputs.filter((i) => i.required)
125
+ .map((i) => i.symbol);
126
+
127
+ describe('signatureOf', () => {
128
+ it('covers every evaluable target, expressions then piecewise then lookups', () => {
129
+ expect(signatureOf(spec).map((s) => s.id)).toEqual([
130
+ 'liquid-release-rate',
131
+ 'transition-pressure',
132
+ 'vapor-sonic',
133
+ 'vapor-subsonic',
134
+ 'vapor-release-rate',
135
+ 'fact_di',
136
+ 'ld_max',
137
+ ]);
138
+ });
139
+
140
+ it('reports what a target produces, with the unit of the answer', () => {
141
+ expect(sigOf('liquid-release-rate')).toMatchObject({
142
+ kind: 'expression',
143
+ produces: 'Wn',
144
+ unit: 'lb/sec',
145
+ equation: '3.3',
146
+ });
147
+ });
148
+
149
+ it('takes a piecewise unit from the symbol it produces, having none itself', () => {
150
+ expect(sigOf('vapor-release-rate')).toMatchObject({ kind: 'piecewise', unit: 'lb/sec' });
151
+ });
152
+
153
+ it('omits constants and derived variables — the caller supplies neither', () => {
154
+ const got = symbols('liquid-release-rate');
155
+ for (const fixed of ['Cd', 'C1', 'gc', 'An']) expect(got).not.toContain(fixed);
156
+ });
157
+
158
+ it('walks THROUGH a derived variable to the inputs it needs', () => {
159
+ // An is derived from d, and d carries a default — so it is offered, not demanded.
160
+ const d = sigOf('liquid-release-rate').inputs.find((i) => i.symbol === 'd');
161
+ expect(d).toMatchObject({ required: false, default: 0.375, unit: 'in' });
162
+ });
163
+
164
+ it('demands an input with no declared default', () => {
165
+ expect(required('liquid-release-rate')).toEqual(['rho_l', 'Pgauge']);
166
+ });
167
+
168
+ it('lists inputs in reading order', () => {
169
+ expect(symbols('liquid-release-rate')).toEqual(['rho_l', 'd', 'Pgauge']);
170
+ });
171
+ });
172
+
173
+ describe('signatureOf — the ladder matches the evaluator', () => {
174
+ it('agrees with evaluateSpec about what is missing', () => {
175
+ // Supply every required input and nothing else: evaluation must succeed.
176
+ const inputs: Record<string, number> = { rho_l: 50, Pgauge: 100 };
177
+ expect(required('liquid-release-rate')).toEqual(Object.keys(inputs));
178
+ const ok = evaluateSpec(spec, 'liquid-release-rate', inputs);
179
+ expect(ok.ok).toBe(true);
180
+
181
+ // Drop one and the evaluator names exactly the symbol the signature did.
182
+ const short = evaluateSpec(spec, 'liquid-release-rate', { rho_l: 50 });
183
+ expect(short.ok).toBe(false);
184
+ if (!short.ok) expect(short.error).toContain("missing required input 'Pgauge'");
185
+ });
186
+
187
+ it('resolves a symbol through the unique target that produces it', () => {
188
+ // Ptrans is produced by transition-pressure, so it is not asked for; the
189
+ // constants that expression needs are not asked for either.
190
+ const got = symbols('vapor-release-rate');
191
+ expect(got).not.toContain('Ptrans');
192
+ expect(got).not.toContain('Patm');
193
+ });
194
+
195
+ it('demands a symbol two targets both produce, as the evaluator does', () => {
196
+ // Wn is produced by BOTH liquid-release-rate and vapor-release-rate.
197
+ const volume = parse({
198
+ ...raw,
199
+ expressions: [...raw.expressions, { id: 'volume', expression: '{Wn} * 2' }],
200
+ });
201
+ const sig = signatureForTarget(volume, 'volume')!;
202
+ const wn = sig.inputs.find((i) => i.symbol === 'Wn');
203
+ expect(wn).toMatchObject({ required: true });
204
+ expect(wn?.note).toContain('produced by more than one target');
205
+ expect(evaluateSpec(volume, 'volume', {}).ok).toBe(false);
206
+ });
207
+
208
+ it('flags a symbol the spec never declares', () => {
209
+ // `detection` and `isolation` are lookup keys with no variable behind them.
210
+ const detection = sigOf('fact_di').inputs.find((i) => i.symbol === 'detection');
211
+ expect(detection).toMatchObject({ required: true, undeclared: true });
212
+ });
213
+
214
+ it('treats an output symbol nothing produces as a required input', () => {
215
+ const orphan = parse({
216
+ ...raw,
217
+ piecewise: [],
218
+ expressions: [{ id: 'only', expression: '{Wn} + 1' }],
219
+ });
220
+ expect(signatureForTarget(orphan, 'only')!.inputs).toEqual([
221
+ { symbol: 'Wn', unit: 'lb/sec', kind: 'number', required: true },
222
+ ]);
223
+ });
224
+ });
225
+
226
+ describe('signatureOf — branches', () => {
227
+ it('unions both branches rather than reporting the one a value would take', () => {
228
+ // Ps gates the branch; MW and Ts are needed whichever way it goes.
229
+ expect(required('vapor-release-rate').sort()).toEqual(['MW', 'Ps', 'Ts']);
230
+ });
231
+
232
+ it('breaks the need down per case', () => {
233
+ const branches = sigOf('vapor-release-rate').branches!;
234
+ expect(branches.map((b) => b.label)).toEqual(['Sonic', 'Subsonic']);
235
+ expect(branches[0]).toMatchObject({ when: '{Ps} > {Ptrans}', use: 'vapor-sonic' });
236
+ expect(branches[0]!.inputs).toContain('MW');
237
+ });
238
+
239
+ it('rolls unverified up to every target that depends on it', () => {
240
+ // The piecewise carries no `unverified` of its own; its branch condition
241
+ // depends on one, and that is what decides whether a number may be relied on.
242
+ expect(sigOf('vapor-release-rate').unverified).toEqual([
243
+ { id: 'transition-pressure', reason: 'Supplied from memory; the source never defines it.' },
244
+ ]);
245
+ expect(sigOf('liquid-release-rate').unverified).toEqual([]);
246
+ });
247
+ });
248
+
249
+ describe('signatureOf — enums', () => {
250
+ it('makes a lookup key an enum over its declared domain', () => {
251
+ expect(sigOf('fact_di').inputs.find((i) => i.symbol === 'isolation')).toMatchObject({
252
+ kind: 'enum',
253
+ domain: ['A', 'B', 'C'],
254
+ });
255
+ });
256
+
257
+ it('falls back to the values the rows actually carry', () => {
258
+ // ld_max declares no domains, so the legal keys are what can match a row.
259
+ expect(sigOf('ld_max').inputs.find((i) => i.symbol === 'holeSize')).toMatchObject({
260
+ kind: 'enum',
261
+ domain: ['1/4 in', '1 in'],
262
+ });
263
+ });
264
+
265
+ it('attaches the criteria prose of the classification named after the symbol', () => {
266
+ const detection = sigOf('fact_di').inputs.find((i) => i.symbol === 'detection');
267
+ expect(detection?.criteria?.A).toContain('Instrumentation designed');
268
+ });
269
+
270
+ it('does NOT attach a rubric to a symbol that merely shares its domain', () => {
271
+ // `isolation` is A|B|C too, but `detection-rating` describes detection.
272
+ expect(sigOf('fact_di').inputs.find((i) => i.symbol === 'isolation')?.criteria).toBeUndefined();
273
+ });
274
+
275
+ it('offers a classification domain even where no lookup declares one', () => {
276
+ const rated = parse({
277
+ ...raw,
278
+ piecewise: [],
279
+ lookups: [],
280
+ expressions: [{ id: 'only', expression: '{detection} == "A"' }],
281
+ });
282
+ expect(signatureForTarget(rated, 'only')!.inputs[0]).toMatchObject({
283
+ symbol: 'detection',
284
+ kind: 'enum',
285
+ domain: ['A', 'B', 'C'],
286
+ });
287
+ });
288
+ });
289
+
290
+ describe('signatureOf — robustness', () => {
291
+ it('terminates on a circular derived chain instead of recursing forever', () => {
292
+ // parseFormulaSpec permits this; the evaluator catches it at run time.
293
+ const cyclic = parse({
294
+ id: 'cyclic',
295
+ variables: [
296
+ { symbol: 'a', role: 'derived', expression: '{b} + 1' },
297
+ { symbol: 'b', role: 'derived', expression: '{a} + 1' },
298
+ ],
299
+ expressions: [{ id: 'go', expression: '{a}' }],
300
+ });
301
+ expect(signatureForTarget(cyclic, 'go')!.inputs).toEqual([]);
302
+ expect(evaluateSpec(cyclic, 'go', {}).ok).toBe(false);
303
+ });
304
+
305
+ it('terminates on a piecewise that names itself', () => {
306
+ const looped = parse({
307
+ id: 'looped',
308
+ variables: [],
309
+ expressions: [{ id: 'e', expression: '1' }],
310
+ piecewise: [{ id: 'p', cases: [{ when: '{x} > 1', use: 'p' }], otherwise: 'e' }],
311
+ });
312
+ expect(signatureForTarget(looped, 'p')!.inputs.map((i) => i.symbol)).toEqual(['x']);
313
+ });
314
+
315
+ it('returns nothing for a target that does not exist', () => {
316
+ expect(signatureForTarget(spec, 'no-such-target')).toBeUndefined();
317
+ });
318
+
319
+ it('survives a spec whose arrays are missing entirely', () => {
320
+ expect(signatureOf({ id: 'bare' } as unknown as FormulaSpec)).toEqual([]);
321
+ });
322
+ });
323
+
324
+ describe('signatureLine', () => {
325
+ it('renders a call line with units, marking optionals', () => {
326
+ expect(signatureLine(sigOf('liquid-release-rate'))).toBe(
327
+ 'liquid-release-rate(rho_l [lb/ft3], d? [in], Pgauge [lbf/in2 (g)]) → Wn [lb/sec]',
328
+ );
329
+ });
330
+
331
+ it('omits the arrow when a target declares no result symbol', () => {
332
+ expect(signatureLine(sigOf('vapor-sonic'))).toBe(
333
+ 'vapor-sonic(d? [in], Ps [lbf/in2 (abs)], MW [lb/lb-mol], Ts [R])',
334
+ );
335
+ });
336
+ });