@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.
- package/LICENSE.md +135 -0
- package/package.json +41 -0
- package/src/block-diff.test.ts +190 -0
- package/src/block-diff.ts +163 -0
- package/src/block-ids.test.ts +358 -0
- package/src/block-ids.ts +242 -0
- package/src/block-list.test.ts +241 -0
- package/src/block-list.ts +177 -0
- package/src/contacts-format.ts +260 -0
- package/src/doc-to-markdown.test.ts +194 -0
- package/src/doc-to-markdown.ts +315 -0
- package/src/formula-dimensions.test.ts +103 -0
- package/src/formula-dimensions.ts +231 -0
- package/src/formula-eval.ts +294 -0
- package/src/formula-seed.test.ts +175 -0
- package/src/formula-seed.ts +466 -0
- package/src/formula-signature.test.ts +336 -0
- package/src/formula-signature.ts +435 -0
- package/src/formula-spec.test.ts +458 -0
- package/src/formula-spec.ts +566 -0
- package/src/journal-options.test.ts +57 -0
- package/src/journal-options.ts +77 -0
- package/src/markdown-refs.test.ts +143 -0
- package/src/markdown-refs.ts +172 -0
- package/src/markdown-to-doc.test.ts +179 -0
- package/src/markdown-to-doc.ts +567 -0
- package/src/onboarding-questions.test.ts +75 -0
- package/src/onboarding-questions.ts +90 -0
- package/src/page-diff.test.ts +82 -0
- package/src/page-diff.ts +120 -0
- package/src/page-split.test.ts +141 -0
- package/src/page-split.ts +128 -0
- package/src/page-toc.test.ts +58 -0
- package/src/page-toc.ts +89 -0
- package/src/persona-bank.test.ts +67 -0
- package/src/persona-bank.ts +234 -0
- package/src/table-formula-mathjs.ts +259 -0
- package/src/table-formula.test.ts +157 -0
- package/src/table-formula.ts +496 -0
- package/src/table-model.test.ts +429 -0
- package/src/table-model.ts +870 -0
- package/src/thinking-tiers.ts +56 -0
- package/tsconfig.json +4 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CALLING CONTRACT for a formula: per evaluable target, what it produces
|
|
3
|
+
* and what a caller must supply to get it.
|
|
4
|
+
*
|
|
5
|
+
* `evaluateSpec` answers "what is the number". This answers the question that
|
|
6
|
+
* comes first and that nothing previously answered: *what must I hand it?* An
|
|
7
|
+
* agent had to read the whole spec and reason out the resolution ladder to
|
|
8
|
+
* guess; a UI evaluator had to re-derive the field list ad hoc; the public
|
|
9
|
+
* share calculator has no way to know at all. All three now read one shape.
|
|
10
|
+
*
|
|
11
|
+
* It is a STATIC MIRROR of `formula-eval.ts`'s resolution order — supplied
|
|
12
|
+
* input → constant → declared default → derived expression → the unique target
|
|
13
|
+
* declaring the symbol as its `resultSymbol`. A symbol is *required* exactly
|
|
14
|
+
* when that ladder cannot reach a value without one being supplied. If the two
|
|
15
|
+
* ever disagree the evaluator is right and this is the bug: it exists to
|
|
16
|
+
* predict the evaluator, not to define anything.
|
|
17
|
+
*
|
|
18
|
+
* Two consequences of being static rather than a dry-run:
|
|
19
|
+
*
|
|
20
|
+
* 1. It sees EVERY branch. A dry-run with placeholder values would report the
|
|
21
|
+
* inputs of whichever piecewise case happened to match, and a form built
|
|
22
|
+
* from that would be missing fields the moment a pressure crossed a
|
|
23
|
+
* threshold. Both branches' needs are unioned; `branches` carries the
|
|
24
|
+
* per-case breakdown so a caller can still say which ones a given path
|
|
25
|
+
* actually costs.
|
|
26
|
+
* 2. It cannot fail on arithmetic. No division by zero, no out-of-domain SQRT,
|
|
27
|
+
* no lookup miss — asking what a formula needs must never itself error.
|
|
28
|
+
*
|
|
29
|
+
* Pure, dependency-free, and NEVER PERSISTED — computed on read, like the
|
|
30
|
+
* rendered text and for the same reason (docs/formulas.md §1): a stored second
|
|
31
|
+
* description of a safety calculation is a copy that can drift from the spec
|
|
32
|
+
* it describes.
|
|
33
|
+
*/
|
|
34
|
+
import { refsIn } from './table-formula';
|
|
35
|
+
import type {
|
|
36
|
+
FormulaSpec,
|
|
37
|
+
FormulaValue,
|
|
38
|
+
SpecClassification,
|
|
39
|
+
SpecExpression,
|
|
40
|
+
SpecLookup,
|
|
41
|
+
SpecPiecewise,
|
|
42
|
+
SpecVariable,
|
|
43
|
+
} from './formula-spec';
|
|
44
|
+
|
|
45
|
+
export type SignatureInputKind = 'number' | 'enum';
|
|
46
|
+
|
|
47
|
+
export interface SignatureInput {
|
|
48
|
+
symbol: string;
|
|
49
|
+
name?: string;
|
|
50
|
+
unit?: string | null;
|
|
51
|
+
/** `enum` when the legal values are known — a lookup key, or a rating. */
|
|
52
|
+
kind: SignatureInputKind;
|
|
53
|
+
/** False when the spec declares a default the evaluator would fall back to. */
|
|
54
|
+
required: boolean;
|
|
55
|
+
/** The value used if nothing is supplied. Only set when `required` is false. */
|
|
56
|
+
default?: FormulaValue;
|
|
57
|
+
/** Legal values, for an `enum`. */
|
|
58
|
+
domain?: FormulaValue[];
|
|
59
|
+
/** Rating → the criterion prose from the source, so a picker can explain the
|
|
60
|
+
* choice rather than offering bare letters. */
|
|
61
|
+
criteria?: Record<string, string>;
|
|
62
|
+
/** Set when the spec never declares this symbol. Evaluation fails with
|
|
63
|
+
* `unknown symbol` unless it is supplied — a spec defect worth surfacing,
|
|
64
|
+
* not a normal input. */
|
|
65
|
+
undeclared?: true;
|
|
66
|
+
/** Why it must be supplied, when the reason is not simply "no default". */
|
|
67
|
+
note?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface SignatureBranch {
|
|
71
|
+
label?: string;
|
|
72
|
+
when: string;
|
|
73
|
+
use: string;
|
|
74
|
+
/** Symbols this case alone needs, so a caller can show the cost of a path. */
|
|
75
|
+
inputs: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface TargetSignature {
|
|
79
|
+
id: string;
|
|
80
|
+
kind: 'expression' | 'piecewise' | 'lookup';
|
|
81
|
+
/** The symbol this target declares as its result, if any. */
|
|
82
|
+
produces?: string;
|
|
83
|
+
unit?: string;
|
|
84
|
+
/** Equation number in the source standard. */
|
|
85
|
+
equation?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Every unverified equation this target's value depends on, itself included.
|
|
88
|
+
* Rolled up rather than reported per-expression because the caller's question
|
|
89
|
+
* is "may I rely on this number", and a piecewise branch carries no
|
|
90
|
+
* `unverified` of its own while depending entirely on one that does.
|
|
91
|
+
*/
|
|
92
|
+
unverified: Array<{ id: string; reason: string }>;
|
|
93
|
+
inputs: SignatureInput[];
|
|
94
|
+
/** Piecewise only. */
|
|
95
|
+
branches?: SignatureBranch[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Suffixes a classification id conventionally carries over its symbol —
|
|
99
|
+
* `detection-rating` describes `detection`. */
|
|
100
|
+
const RATING_SUFFIXES = ['rating', 'ratings', 'classification', 'class', 'level', 'grade'];
|
|
101
|
+
|
|
102
|
+
const norm = (s: string): string => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Which classification describes this symbol, if any.
|
|
106
|
+
*
|
|
107
|
+
* Nothing in the spec LINKS a classification to a symbol — the schema has no
|
|
108
|
+
* such field, and adding one would invalidate every spec already stored. The
|
|
109
|
+
* link is by name, the way authors already write it: the API RP 581 model's
|
|
110
|
+
* `detection-rating` rubric is the one that explains the `detection` lookup
|
|
111
|
+
* key. So this matches on the normalised id with a trailing rating-ish word
|
|
112
|
+
* removed, and additionally requires the classification to cover every value
|
|
113
|
+
* actually in play.
|
|
114
|
+
*
|
|
115
|
+
* Deliberately tight, and ambiguity yields nothing. The wrong rubric attached
|
|
116
|
+
* to a rating would put misleading prose in front of whoever picks the value —
|
|
117
|
+
* and in that model `detection` and `isolation` share the domain `A|B|C`, so
|
|
118
|
+
* matching on domain shape alone would confidently offer detection criteria
|
|
119
|
+
* for the isolation field. A missing rubric costs a picker its help text;
|
|
120
|
+
* never a number, since classifications take no part in arithmetic.
|
|
121
|
+
*/
|
|
122
|
+
function classificationFor(
|
|
123
|
+
classifications: SpecClassification[],
|
|
124
|
+
symbol: string,
|
|
125
|
+
domain: FormulaValue[] | undefined,
|
|
126
|
+
): SpecClassification | undefined {
|
|
127
|
+
const want = norm(symbol);
|
|
128
|
+
if (!want) return undefined;
|
|
129
|
+
const matches = classifications.filter((c) => {
|
|
130
|
+
const id = norm(c.id ?? '');
|
|
131
|
+
if (!id) return false;
|
|
132
|
+
if (id === want) return true;
|
|
133
|
+
return RATING_SUFFIXES.some(
|
|
134
|
+
(suffix) =>
|
|
135
|
+
id.length > suffix.length && id.endsWith(suffix) && id.slice(0, -suffix.length) === want,
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
if (matches.length !== 1) return undefined;
|
|
139
|
+
const found = matches[0]!;
|
|
140
|
+
if (domain && !domain.every((v) => found.domain.includes(String(v)))) return undefined;
|
|
141
|
+
return found;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The values a lookup key can take: the declared domain, else exactly the
|
|
145
|
+
* values its rows carry — which is the set that can actually match. */
|
|
146
|
+
function domainOfKey(lookup: SpecLookup, key: string): FormulaValue[] {
|
|
147
|
+
const declared = lookup.domains?.[key];
|
|
148
|
+
if (Array.isArray(declared) && declared.length > 0) return declared;
|
|
149
|
+
const out: FormulaValue[] = [];
|
|
150
|
+
const seen = new Set<string>();
|
|
151
|
+
for (const row of lookup.rows ?? []) {
|
|
152
|
+
const value = row?.[key];
|
|
153
|
+
if (value === undefined) continue;
|
|
154
|
+
const marker = `${typeof value}:${String(value)}`;
|
|
155
|
+
if (seen.has(marker)) continue;
|
|
156
|
+
seen.add(marker);
|
|
157
|
+
out.push(value);
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Never throws: a malformed expression that slipped past validation must not
|
|
163
|
+
* take down "what does this need". */
|
|
164
|
+
function safeRefs(expression: string | undefined): string[] {
|
|
165
|
+
if (!expression) return [];
|
|
166
|
+
try {
|
|
167
|
+
return refsIn(expression);
|
|
168
|
+
} catch {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
type EnumContext = {
|
|
174
|
+
domain: FormulaValue[];
|
|
175
|
+
/** The lookup tolerates values outside the domain (`onMiss: 'null'`) —
|
|
176
|
+
* the enum is then guidance, not law, and the input says so. */
|
|
177
|
+
open?: boolean;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
class SignatureWalker {
|
|
181
|
+
private readonly variables = new Map<string, SpecVariable>();
|
|
182
|
+
/** resultSymbol → ids of targets declaring it, mirroring the evaluator. */
|
|
183
|
+
private readonly producers = new Map<string, string[]>();
|
|
184
|
+
private readonly expressions = new Map<string, SpecExpression>();
|
|
185
|
+
private readonly piecewise = new Map<string, SpecPiecewise>();
|
|
186
|
+
private readonly lookups = new Map<string, SpecLookup>();
|
|
187
|
+
private readonly classifications: SpecClassification[];
|
|
188
|
+
|
|
189
|
+
constructor(private readonly spec: FormulaSpec) {
|
|
190
|
+
for (const v of spec.variables ?? []) if (v?.symbol) this.variables.set(v.symbol, v);
|
|
191
|
+
for (const e of spec.expressions ?? []) if (e?.id) this.expressions.set(e.id, e);
|
|
192
|
+
for (const p of spec.piecewise ?? []) if (p?.id) this.piecewise.set(p.id, p);
|
|
193
|
+
for (const l of spec.lookups ?? []) if (l?.id) this.lookups.set(l.id, l);
|
|
194
|
+
this.classifications = (spec.classifications ?? []).filter(Boolean);
|
|
195
|
+
|
|
196
|
+
const claim = (symbol: string | undefined, id: string) => {
|
|
197
|
+
if (!symbol) return;
|
|
198
|
+
this.producers.set(symbol, [...(this.producers.get(symbol) ?? []), id]);
|
|
199
|
+
};
|
|
200
|
+
for (const e of spec.expressions ?? []) claim(e?.resultSymbol, e?.id ?? '');
|
|
201
|
+
for (const p of spec.piecewise ?? []) claim(p?.resultSymbol, p?.id ?? '');
|
|
202
|
+
for (const l of spec.lookups ?? []) claim(l?.resultSymbol, l?.id ?? '');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
targetIds(): string[] {
|
|
206
|
+
return [...this.expressions.keys(), ...this.piecewise.keys(), ...this.lookups.keys()];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
signatureFor(id: string): TargetSignature {
|
|
210
|
+
const scope = new Scope();
|
|
211
|
+
this.walkTarget(id, scope);
|
|
212
|
+
|
|
213
|
+
const expression = this.expressions.get(id);
|
|
214
|
+
const piecewise = this.piecewise.get(id);
|
|
215
|
+
const lookup = this.lookups.get(id);
|
|
216
|
+
const produces = expression?.resultSymbol ?? piecewise?.resultSymbol ?? lookup?.resultSymbol;
|
|
217
|
+
// A piecewise or lookup carries no unit of its own, but the symbol it
|
|
218
|
+
// produces usually does — that is the unit of the answer either way.
|
|
219
|
+
const unit =
|
|
220
|
+
expression?.unit ??
|
|
221
|
+
(produces ? (this.variables.get(produces)?.unit ?? undefined) : undefined);
|
|
222
|
+
|
|
223
|
+
const sig: TargetSignature = {
|
|
224
|
+
id,
|
|
225
|
+
kind: expression ? 'expression' : piecewise ? 'piecewise' : 'lookup',
|
|
226
|
+
...(produces ? { produces } : {}),
|
|
227
|
+
...(unit ? { unit } : {}),
|
|
228
|
+
...(expression?.equation ? { equation: expression.equation } : {}),
|
|
229
|
+
unverified: scope.unverified,
|
|
230
|
+
inputs: [...scope.inputs.values()],
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
if (piecewise) {
|
|
234
|
+
sig.branches = (piecewise.cases ?? []).map((c) => {
|
|
235
|
+
const branch = new Scope();
|
|
236
|
+
for (const ref of safeRefs(c.when)) this.walkSymbol(ref, branch, undefined);
|
|
237
|
+
this.walkTarget(c.use, branch);
|
|
238
|
+
return {
|
|
239
|
+
...(c.label ? { label: c.label } : {}),
|
|
240
|
+
when: c.when,
|
|
241
|
+
use: c.use,
|
|
242
|
+
inputs: [...branch.inputs.keys()],
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return sig;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private walkTarget(id: string, scope: Scope): void {
|
|
250
|
+
if (scope.targets.has(id)) return;
|
|
251
|
+
scope.targets.add(id);
|
|
252
|
+
|
|
253
|
+
const expression = this.expressions.get(id);
|
|
254
|
+
if (expression) {
|
|
255
|
+
if (expression.unverified) {
|
|
256
|
+
scope.addUnverified(expression.id, expression.unverified);
|
|
257
|
+
}
|
|
258
|
+
for (const ref of safeRefs(expression.expression)) this.walkSymbol(ref, scope, undefined);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const piecewise = this.piecewise.get(id);
|
|
263
|
+
if (piecewise) {
|
|
264
|
+
for (const c of piecewise.cases ?? []) {
|
|
265
|
+
for (const ref of safeRefs(c.when)) this.walkSymbol(ref, scope, undefined);
|
|
266
|
+
this.walkTarget(c.use, scope);
|
|
267
|
+
}
|
|
268
|
+
if (piecewise.otherwise) this.walkTarget(piecewise.otherwise, scope);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const lookup = this.lookups.get(id);
|
|
273
|
+
if (lookup) {
|
|
274
|
+
for (const key of lookup.keys ?? []) {
|
|
275
|
+
this.walkSymbol(key, scope, {
|
|
276
|
+
domain: domainOfKey(lookup, key),
|
|
277
|
+
// `onMiss: 'null'` means an unmatched key is DEFINED behaviour (the
|
|
278
|
+
// lookup yields null), so the enum must present itself as guidance
|
|
279
|
+
// rather than the law the evaluator will enforce.
|
|
280
|
+
open: lookup.onMiss === 'null',
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// An unknown id contributes nothing — `evaluateSpec` reports it, and a
|
|
285
|
+
// signature that invented inputs for a target that does not exist would be
|
|
286
|
+
// worse than an empty one.
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** The evaluator's `resolveSymbol` ladder, walked for requiredness. */
|
|
290
|
+
private walkSymbol(symbol: string, scope: Scope, enumCtx: EnumContext | undefined): void {
|
|
291
|
+
const already = scope.inputs.get(symbol);
|
|
292
|
+
if (already) {
|
|
293
|
+
// Reached again with better information: the same symbol can be plain
|
|
294
|
+
// arithmetic in one expression and a lookup key in another.
|
|
295
|
+
if (enumCtx) this.applyEnum(already, symbol, enumCtx);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// Also guards cycles — a derived symbol re-entering itself finds its own
|
|
299
|
+
// mark and stops, where the evaluator raises `circular reference`.
|
|
300
|
+
if (scope.symbols.has(symbol)) return;
|
|
301
|
+
scope.symbols.add(symbol);
|
|
302
|
+
|
|
303
|
+
const variable = this.variables.get(symbol);
|
|
304
|
+
|
|
305
|
+
if (variable?.role === 'constant') return; // fixed by the spec
|
|
306
|
+
if (variable?.role === 'derived') {
|
|
307
|
+
for (const ref of safeRefs(variable.expression)) this.walkSymbol(ref, scope, undefined);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (variable?.role === 'input') {
|
|
311
|
+
const hasDefault = variable.value !== undefined && variable.value !== null;
|
|
312
|
+
this.addInput(scope, symbol, variable, enumCtx, {
|
|
313
|
+
required: !hasDefault,
|
|
314
|
+
...(hasDefault ? { default: variable.value as FormulaValue } : {}),
|
|
315
|
+
});
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// `output`, or a symbol the spec never declares: the evaluator falls
|
|
320
|
+
// through to whatever target produces it.
|
|
321
|
+
const producedBy = this.producers.get(symbol) ?? [];
|
|
322
|
+
if (producedBy.length === 1) {
|
|
323
|
+
this.walkTarget(producedBy[0]!, scope);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (producedBy.length > 1) {
|
|
327
|
+
this.addInput(scope, symbol, variable, enumCtx, {
|
|
328
|
+
required: true,
|
|
329
|
+
note: `produced by more than one target (${producedBy.join(', ')}) — supply it to say which`,
|
|
330
|
+
});
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
this.addInput(scope, symbol, variable, enumCtx, {
|
|
334
|
+
required: true,
|
|
335
|
+
...(variable ? {} : { undeclared: true as const }),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private addInput(
|
|
340
|
+
scope: Scope,
|
|
341
|
+
symbol: string,
|
|
342
|
+
variable: SpecVariable | undefined,
|
|
343
|
+
enumCtx: EnumContext | undefined,
|
|
344
|
+
extra: Partial<SignatureInput> & { required: boolean },
|
|
345
|
+
): void {
|
|
346
|
+
const input: SignatureInput = {
|
|
347
|
+
symbol,
|
|
348
|
+
...(variable?.name ? { name: variable.name } : {}),
|
|
349
|
+
...(variable?.unit ? { unit: variable.unit } : {}),
|
|
350
|
+
kind: 'number',
|
|
351
|
+
...extra,
|
|
352
|
+
};
|
|
353
|
+
this.applyEnum(input, symbol, enumCtx);
|
|
354
|
+
scope.inputs.set(symbol, input);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Turn an input into an `enum` when its legal values are knowable — from the
|
|
359
|
+
* lookup it keys, or from a classification named after it. Without this a
|
|
360
|
+
* rating renders as a free-text box, and a case-typo becomes a runtime error
|
|
361
|
+
* where it could have been an impossible one.
|
|
362
|
+
*/
|
|
363
|
+
private applyEnum(input: SignatureInput, symbol: string, enumCtx: EnumContext | undefined): void {
|
|
364
|
+
const domain = enumCtx?.domain?.length ? enumCtx.domain : undefined;
|
|
365
|
+
const classification = classificationFor(this.classifications, symbol, domain);
|
|
366
|
+
const values = domain ?? (classification ? [...classification.domain] : undefined);
|
|
367
|
+
if (!values?.length) return;
|
|
368
|
+
input.kind = 'enum';
|
|
369
|
+
input.domain = values;
|
|
370
|
+
if (enumCtx?.open && !input.note) {
|
|
371
|
+
input.note = 'Other values are accepted; an unmatched one yields an empty result.';
|
|
372
|
+
}
|
|
373
|
+
if (classification) {
|
|
374
|
+
const criteria: Record<string, string> = {};
|
|
375
|
+
for (const value of values) {
|
|
376
|
+
const text = classification.criteria?.[String(value)];
|
|
377
|
+
if (text) criteria[String(value)] = text;
|
|
378
|
+
}
|
|
379
|
+
if (Object.keys(criteria).length > 0) input.criteria = criteria;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Per-target accumulation. Insertion order is source order, which is reading
|
|
385
|
+
* order — the order an author would list the inputs themselves. */
|
|
386
|
+
class Scope {
|
|
387
|
+
readonly inputs = new Map<string, SignatureInput>();
|
|
388
|
+
readonly symbols = new Set<string>();
|
|
389
|
+
readonly targets = new Set<string>();
|
|
390
|
+
readonly unverified: Array<{ id: string; reason: string }> = [];
|
|
391
|
+
private readonly unverifiedIds = new Set<string>();
|
|
392
|
+
|
|
393
|
+
addUnverified(id: string, reason: string): void {
|
|
394
|
+
if (this.unverifiedIds.has(id)) return;
|
|
395
|
+
this.unverifiedIds.add(id);
|
|
396
|
+
this.unverified.push({ id, reason });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* The calling contract for every evaluable target of a spec, in the order
|
|
402
|
+
* `formula_get` lists them: expressions, then piecewise, then lookups.
|
|
403
|
+
*/
|
|
404
|
+
export function signatureOf(spec: FormulaSpec): TargetSignature[] {
|
|
405
|
+
const walker = new SignatureWalker(spec);
|
|
406
|
+
return walker.targetIds().map((id) => walker.signatureFor(id));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** One target's contract, or undefined if the id is not evaluable. */
|
|
410
|
+
export function signatureForTarget(
|
|
411
|
+
spec: FormulaSpec,
|
|
412
|
+
targetId: string,
|
|
413
|
+
): TargetSignature | undefined {
|
|
414
|
+
const walker = new SignatureWalker(spec);
|
|
415
|
+
return walker.targetIds().includes(targetId) ? walker.signatureFor(targetId) : undefined;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* One target rendered as a call line — `vapor-sonic(Ps [lbf/in2 (abs)], MW,
|
|
420
|
+
* Ts [R]) → Wn [lb/sec]`. Used by `formulaToText` so the embedding captures
|
|
421
|
+
* what a formula can COMPUTE, not only what it says; "can it work out a
|
|
422
|
+
* release rate from pressure and temperature?" is a question about the
|
|
423
|
+
* signature, and until it was indexed nothing could answer it.
|
|
424
|
+
*/
|
|
425
|
+
export function signatureLine(sig: TargetSignature): string {
|
|
426
|
+
const args = sig.inputs
|
|
427
|
+
.map((i) => {
|
|
428
|
+
const unit = i.unit ? ` [${i.unit}]` : '';
|
|
429
|
+
const optional = i.required ? '' : '?';
|
|
430
|
+
return `${i.symbol}${optional}${unit}`;
|
|
431
|
+
})
|
|
432
|
+
.join(', ');
|
|
433
|
+
const produces = sig.produces ? ` → ${sig.produces}${sig.unit ? ` [${sig.unit}]` : ''}` : '';
|
|
434
|
+
return `${sig.id}(${args})${produces}`;
|
|
435
|
+
}
|