@gobing-ai/ts-decision-fm 0.5.30 → 0.5.32

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/README.md CHANGED
@@ -10,6 +10,15 @@ frequencies — the model is never asked for a confidence
10
10
 
11
11
  Part of the `@gobing-ai/ts-libs` monorepo and lockstep-versioned with it.
12
12
 
13
+ **Built on the shared sampled-driver core.** Since task 0114 the engine-agnostic
14
+ half — prompt rendering, enum schema, answer parsing and the frequency
15
+ estimators — lives in `@gobing-ai/ts-ai-runner`
16
+ (`src/decision/sampled-driver.ts`). This package keeps only the `fm` process
17
+ bridge: the availability probe, the prompt-token budget and the per-ask schema
18
+ temp file. **Every export is unchanged** — `FM_INSTRUCTIONS`,
19
+ `buildFmSchema`, `FmSchema`, `formatDesc`, `declaredOptions`, `buildPrompt`, the
20
+ four estimators and `requireEnumValue` are re-exported or aliased from the core.
21
+
13
22
  - **License:** Apache-2.0 (see [LICENSE](./LICENSE)).
14
23
  - **No model weights or third-party code are bundled.** The engine is the `fm`
15
24
  system binary on the host.
package/dist/driver.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
- * The Apple `fm` decision driver (task 0084 R2, R8, R9; design § Main export).
3
- * Satisfies the same {@link DecisionDriver} contract as the other backends over
4
- * one-shot `fm` processes: one schema per ask answers the whole question map,
5
- * probabilities come from k sample frequencies (ADR-030), and every failure
6
- * surfaces through the decision error taxonomy — never as an answer.
2
+ * The Apple `fm` decision driver (task 0084 R2, R8, R9; task 0114 R7). Satisfies
3
+ * the same {@link DecisionDriver} contract as the other backends over one-shot
4
+ * `fm` processes, built on the shared sampled-driver core: one schema per ask
5
+ * answers the whole question map, probabilities come from k sample frequencies
6
+ * (ADR-030), and every failure surfaces through the decision error taxonomy —
7
+ * never as an answer.
7
8
  */
8
9
  import { type DecisionDriver } from '@gobing-ai/ts-ai-runner';
9
10
  import { type ProcessExecutor } from '@gobing-ai/ts-runtime';
@@ -1 +1 @@
1
- {"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../src/driver.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAGH,KAAK,cAAc,EAItB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAKH,KAAK,eAAe,EACvB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAEH,KAAK,YAAY,EAMpB,MAAM,cAAc,CAAC;AAGtB,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC5B,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,0FAA0F;IAC1F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,iCAAiC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,kGAAkG;IAClG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,mFAAmF;AACnF,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACpD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,SAAS,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC;CACjH;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,eAAoB,GAAG,gBAAgB,CAwI9E"}
1
+ {"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../src/driver.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAMH,KAAK,cAAc,EAItB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAKH,KAAK,eAAe,EACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAEH,KAAK,YAAY,EAKpB,MAAM,cAAc,CAAC;AAEtB,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC5B,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,0FAA0F;IAC1F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,iCAAiC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,kGAAkG;IAClG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,mFAAmF;AACnF,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACpD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,SAAS,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC;CACjH;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,eAAoB,GAAG,gBAAgB,CA2G9E"}
package/dist/driver.js CHANGED
@@ -1,15 +1,14 @@
1
1
  /**
2
- * The Apple `fm` decision driver (task 0084 R2, R8, R9; design § Main export).
3
- * Satisfies the same {@link DecisionDriver} contract as the other backends over
4
- * one-shot `fm` processes: one schema per ask answers the whole question map,
5
- * probabilities come from k sample frequencies (ADR-030), and every failure
6
- * surfaces through the decision error taxonomy — never as an answer.
2
+ * The Apple `fm` decision driver (task 0084 R2, R8, R9; task 0114 R7). Satisfies
3
+ * the same {@link DecisionDriver} contract as the other backends over one-shot
4
+ * `fm` processes, built on the shared sampled-driver core: one schema per ask
5
+ * answers the whole question map, probabilities come from k sample frequencies
6
+ * (ADR-030), and every failure surfaces through the decision error taxonomy —
7
+ * never as an answer.
7
8
  */
8
- import { DecisionConfigError, DecisionRequestError, } from '@gobing-ai/ts-ai-runner';
9
+ import { buildEnumSchema, createSampledDriver, DECISION_INSTRUCTIONS, DecisionConfigError, DecisionRequestError, } from '@gobing-ai/ts-ai-runner';
9
10
  import { createNodeFileSystem, getProcessEnv, joinPath, nodeBunFactory, } from '@gobing-ai/ts-runtime';
10
- import { estimateChoice, estimateNoul, estimateScore } from './estimate.js';
11
- import { countPromptTokens, parseFmRespond, probeFmAvailability, requireEnumValue, respondArgv, runFmRespond, } from './fm-process.js';
12
- import { buildFmSchema, buildPrompt, declaredOptions, FM_INSTRUCTIONS } from './schema.js';
11
+ import { countPromptTokens, parseFmRespond, probeFmAvailability, respondArgv, runFmRespond, } from './fm-process.js';
13
12
  /**
14
13
  * Build the `fm-local` decision driver. Host platform/architecture is checked
15
14
  * here, at construction, without spawning (R8); the `fm` availability probe
@@ -17,10 +16,10 @@ import { buildFmSchema, buildPrompt, declaredOptions, FM_INSTRUCTIONS } from './
17
16
  */
18
17
  export function createFmDriver(options = {}) {
19
18
  const { samples = 5, deterministic = false, maxPromptTokens = 6_000, requestTimeoutMs = 30_000, guardrails, fmPath = 'fm', executor = nodeBunFactory.createProcessExecutor(), platform = process.platform, arch = process.arch, } = options;
19
+ // Order kept from the pre-refactor driver: the samples error precedes PLATFORM.
20
20
  if (!Number.isInteger(samples) || samples < 1) {
21
21
  throw new DecisionRequestError(`samples must be a positive integer; got ${samples}`, undefined, undefined);
22
22
  }
23
- const k = deterministic ? 1 : samples;
24
23
  if (platform !== 'darwin' || arch !== 'arm64') {
25
24
  throw new DecisionConfigError(`decision-fm requires macOS on Apple Silicon (darwin arm64); current platform is '${platform} ${arch}'`, 'PLATFORM');
26
25
  }
@@ -32,74 +31,53 @@ export function createFmDriver(options = {}) {
32
31
  await probeFmAvailability(executor, fmPath, requestTimeoutMs);
33
32
  availabilityVerified = true;
34
33
  }
35
- return {
34
+ const inner = createSampledDriver({
36
35
  name: 'fm-local',
37
- estimator: { kind: 'sample-frequency', samples: k, greedy: deterministic },
38
- async ask({ state, questions, model, }) {
39
- // R9: only the on-device system model exists (design: fm CLI facts).
40
- if (model !== undefined && model !== 'system') {
41
- throw new DecisionRequestError(`fm-local supports only the 'system' model (or undefined); got '${model}'`, undefined, undefined);
42
- }
43
- const entries = Object.entries(questions);
44
- if (entries.length === 0) {
45
- throw new DecisionRequestError('expected a nonempty question map', undefined, undefined);
46
- }
47
- for (const [key, question] of entries) {
48
- const optionCount = question.kind === 'choice'
49
- ? Object.keys(question.labels).length
50
- : question.kind === 'score'
51
- ? question.rubric.length
52
- : 2; // noul is binary by type
53
- if (optionCount < 2) {
54
- throw new DecisionRequestError(`question '${key}' has fewer than two options (${optionCount})`, undefined, undefined);
55
- }
56
- }
36
+ sampleLabel: 'fm',
37
+ samples,
38
+ deterministic,
39
+ instructions: DECISION_INSTRUCTIONS,
40
+ async open({ instructions, prompt, questions }) {
57
41
  // R8: prerequisite probe before the first sample; cached per driver.
58
42
  await ensureFmAvailable();
59
- const instructions = FM_INSTRUCTIONS;
60
- const prompt = buildPrompt(state, questions);
61
43
  // R4: pre-flight budget — over budget rejects before any `fm respond`.
62
44
  const tokens = await countPromptTokens(executor, fmPath, instructions, prompt, requestTimeoutMs);
63
45
  if (tokens > maxPromptTokens) {
64
46
  throw new DecisionRequestError(`prompt is ${tokens} tokens, above the maxPromptTokens budget of ${maxPromptTokens}`, undefined, undefined);
65
47
  }
66
48
  // R3: one schema per ask over the whole question map, in a uniquely
67
- // named temp file, deleted in `finally` on success and failure.
49
+ // named temp file, deleted by `close` on success and failure.
68
50
  const schemaPath = joinPath(getProcessEnv().TMPDIR ?? '/tmp', `fm-schema-${crypto.randomUUID()}.json`);
69
- try {
70
- fs.writeFile(schemaPath, JSON.stringify(buildFmSchema(questions)));
71
- // R5: k sequential samples; each stdout parsed as JSON, read by key.
72
- const allowed = new Map(entries.map(([key, question]) => [key, declaredOptions(question)]));
73
- const samplesByKey = new Map(entries.map(([key]) => [key, []]));
74
- for (let i = 0; i < k; i++) {
51
+ const session = {
52
+ async sample() {
75
53
  const stdout = await runFmRespond(executor, fmPath, respondArgv({ schemaPath, instructions, prompt, greedy: deterministic, guardrails }), requestTimeoutMs);
76
- const parsed = parseFmRespond(stdout);
77
- for (const [key, values] of samplesByKey) {
78
- values.push(requireEnumValue(parsed, key, allowed.get(key) ?? []));
79
- }
80
- }
81
- // R6: frequencies over the declared labels/levels, argmax with
82
- // declaration-order / lower-level tie-break, entropy confidence.
83
- const answers = {};
84
- for (const [key, question] of entries) {
85
- const values = samplesByKey.get(key) ?? [];
86
- switch (question.kind) {
87
- case 'choice':
88
- answers[key] = estimateChoice(Object.keys(question.labels), values);
89
- break;
90
- case 'score':
91
- answers[key] = estimateScore(question.rubric, values);
92
- break;
93
- case 'noul':
94
- answers[key] = estimateNoul(values);
95
- break;
96
- }
97
- }
98
- return answers;
54
+ return parseFmRespond(stdout);
55
+ },
56
+ close() {
57
+ fs.deleteFile(schemaPath);
58
+ },
59
+ };
60
+ try {
61
+ fs.writeFile(schemaPath, JSON.stringify(buildEnumSchema(questions)));
99
62
  }
100
- finally {
63
+ catch (error) {
64
+ // Mirrors the pre-refactor `finally`: a failed write still deletes.
101
65
  fs.deleteFile(schemaPath);
66
+ throw error;
67
+ }
68
+ return session;
69
+ },
70
+ });
71
+ return {
72
+ name: 'fm-local',
73
+ estimator: inner.estimator,
74
+ // R9: only the on-device system model exists (design: fm CLI facts). The
75
+ // guard runs before the core so the model error keeps today's precedence.
76
+ async ask({ state, questions, model, }) {
77
+ if (model !== undefined && model !== 'system') {
78
+ throw new DecisionRequestError(`fm-local supports only the 'system' model (or undefined); got '${model}'`, undefined, undefined);
102
79
  }
80
+ return inner.ask({ state, questions, model });
103
81
  },
104
82
  };
105
83
  }
@@ -1,26 +1,7 @@
1
1
  /**
2
- * Probability estimation from repeated samples (task 0084 R6, ADR-030,
3
- * design § Probability estimation). Every probability is an empirical sample
4
- * frequency — never a self-reported confidence — so the resolution limit is
5
- * 1/k and `confidence` measures how much the k samples agreed, not calibration.
2
+ * Estimator aliases (task 0114 R8). The bodies moved to the dependency-free
3
+ * sampled-driver core in `@gobing-ai/ts-ai-runner`; the fm names stay exported
4
+ * so nothing that imported this module changes.
6
5
  */
7
- import type { ChoiceAnswer, Desc, NoulAnswer, ScoreAnswer } from '@gobing-ai/ts-ai-runner';
8
- /**
9
- * Laya reference `confidence_from_probs` on the empirical distribution:
10
- * `1 − H(p) / ln(n)` over the n declared options. One-hot ⇒ 1, uniform ⇒ 0.
11
- */
12
- export declare function entropyConfidence(probabilities: number[]): number;
13
- /**
14
- * Choice frequencies over every supplied label (unsampled ⇒ 0). The answer
15
- * label is the most frequent with ties broken by label declaration order.
16
- */
17
- export declare function estimateChoice(labels: readonly string[], samples: readonly string[]): ChoiceAnswer<string>;
18
- /**
19
- * Score frequencies over the level indices `0…n-1`. The answer score is the
20
- * most frequent level with ties going to the lower level; the legend maps each
21
- * index back to its rubric description.
22
- */
23
- export declare function estimateScore(rubric: readonly Desc[], samples: readonly string[]): ScoreAnswer;
24
- /** Noul is a bare yes-probability — no `confidence` key, by contract. */
25
- export declare function estimateNoul(samples: readonly string[]): NoulAnswer;
6
+ export { entropyConfidence, estimateChoice, estimateNoul, estimateScore } from '@gobing-ai/ts-ai-runner';
26
7
  //# sourceMappingURL=estimate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"estimate.d.ts","sourceRoot":"","sources":["../src/estimate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3F;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,MAAM,CAQjE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,YAAY,CAAC,MAAM,CAAC,CAyB1G;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,IAAI,EAAE,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,WAAW,CAiC9F;AAED,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CAGnE"}
1
+ {"version":3,"file":"estimate.d.ts","sourceRoot":"","sources":["../src/estimate.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC"}
package/dist/estimate.js CHANGED
@@ -1,90 +1,6 @@
1
1
  /**
2
- * Laya reference `confidence_from_probs` on the empirical distribution:
3
- * `1 − H(p) / ln(n)` over the n declared options. One-hot ⇒ 1, uniform ⇒ 0.
2
+ * Estimator aliases (task 0114 R8). The bodies moved to the dependency-free
3
+ * sampled-driver core in `@gobing-ai/ts-ai-runner`; the fm names stay exported
4
+ * so nothing that imported this module changes.
4
5
  */
5
- export function entropyConfidence(probabilities) {
6
- const n = probabilities.length;
7
- if (n <= 1)
8
- return 1;
9
- let entropy = 0;
10
- for (const p of probabilities) {
11
- if (p > 0)
12
- entropy -= p * Math.log(p);
13
- }
14
- return 1 - entropy / Math.log(n);
15
- }
16
- /**
17
- * Choice frequencies over every supplied label (unsampled ⇒ 0). The answer
18
- * label is the most frequent with ties broken by label declaration order.
19
- */
20
- export function estimateChoice(labels, samples) {
21
- const counts = new Map(labels.map((label) => [label, 0]));
22
- for (const sample of samples)
23
- counts.set(sample, (counts.get(sample) ?? 0) + 1);
24
- const k = samples.length;
25
- const probabilities = {};
26
- const distribution = [];
27
- let bestLabel = labels[0] ?? '';
28
- let bestCount = -1;
29
- for (const label of labels) {
30
- const count = counts.get(label) ?? 0;
31
- const probability = count / k;
32
- distribution.push(probability);
33
- probabilities[label] = probability;
34
- // Declaration order wins ties: strict `>` keeps the earlier label.
35
- if (count > bestCount) {
36
- bestCount = count;
37
- bestLabel = label;
38
- }
39
- }
40
- return {
41
- kind: 'choice',
42
- label: bestLabel,
43
- confidence: entropyConfidence(distribution),
44
- probabilities,
45
- };
46
- }
47
- /**
48
- * Score frequencies over the level indices `0…n-1`. The answer score is the
49
- * most frequent level with ties going to the lower level; the legend maps each
50
- * index back to its rubric description.
51
- */
52
- export function estimateScore(rubric, samples) {
53
- const k = samples.length;
54
- const counts = [];
55
- for (const sample of samples) {
56
- const level = Number.parseInt(sample, 10);
57
- counts[level] = (counts[level] ?? 0) + 1;
58
- }
59
- const probabilities = {};
60
- const distribution = [];
61
- let bestLevel = 0;
62
- let bestCount = -1;
63
- for (let level = 0; level < rubric.length; level++) {
64
- const count = counts[level] ?? 0;
65
- const probability = count / k;
66
- distribution.push(probability);
67
- probabilities[level] = probability;
68
- // Lower level wins ties: strict `>` keeps the smaller index.
69
- if (count > bestCount) {
70
- bestCount = count;
71
- bestLevel = level;
72
- }
73
- }
74
- const legend = {};
75
- rubric.forEach((desc, level) => {
76
- legend[level] = desc;
77
- });
78
- return {
79
- kind: 'score',
80
- score: bestLevel,
81
- confidence: entropyConfidence(distribution),
82
- legend,
83
- probabilities,
84
- };
85
- }
86
- /** Noul is a bare yes-probability — no `confidence` key, by contract. */
87
- export function estimateNoul(samples) {
88
- const yes = samples.filter((sample) => sample === 'yes').length;
89
- return { kind: 'noul', probability: yes / samples.length };
90
- }
6
+ export { entropyConfidence, estimateChoice, estimateNoul, estimateScore } from '@gobing-ai/ts-ai-runner';
@@ -56,5 +56,5 @@ export declare function runFmRespond(executor: ProcessExecutor, fmPath: string,
56
56
  */
57
57
  export declare function parseFmRespond(stdout: string): Record<string, string>;
58
58
  /** A parsed value outside the declared enum is a schema violation, never an answer (R7). */
59
- export declare function requireEnumValue(parsed: Record<string, string>, key: string, allowed: readonly string[]): string;
59
+ export declare function requireEnumValue(parsed: Record<string, string>, key: string, allowed: readonly string[], label?: string): string;
60
60
  //# sourceMappingURL=fm-process.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fm-process.d.ts","sourceRoot":"","sources":["../src/fm-process.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,OAAO,KAAK,EAAE,eAAe,EAAiB,MAAM,uBAAuB,CAAC;AAE5E,6EAA6E;AAC7E,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,oCAAoC,CAAC;AAE5E;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAE9E;AAED,mJAAmJ;AACnJ,wBAAgB,WAAW,CAAC,OAAO,EAAE;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sDAAsD;IACtD,UAAU,CAAC,EAAE,YAAY,CAAC;CAC7B,GAAG,MAAM,EAAE,CAYX;AAED,kFAAkF;AAClF,wBAAgB,aAAa,IAAI,MAAM,EAAE,CAExC;AAaD;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACrC,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,MAAM,EACd,SAAS,SAAS,GACnB,OAAO,CAAC,IAAI,CAAC,CAWf;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACnC,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,SAAS,SAAS,GACnB,OAAO,CAAC,MAAM,CAAC,CA0BjB;AAMD;;;;GAIG;AACH,wBAAsB,YAAY,CAC9B,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EAAE,EACd,gBAAgB,EAAE,MAAM,GACzB,OAAO,CAAC,MAAM,CAAC,CAwBjB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAoBrE;AAED,4FAA4F;AAC5F,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAShH"}
1
+ {"version":3,"file":"fm-process.d.ts","sourceRoot":"","sources":["../src/fm-process.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH,OAAO,KAAK,EAAE,eAAe,EAAiB,MAAM,uBAAuB,CAAC;AAE5E,6EAA6E;AAC7E,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,oCAAoC,CAAC;AAE5E;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAE9E;AAED,mJAAmJ;AACnJ,wBAAgB,WAAW,CAAC,OAAO,EAAE;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sDAAsD;IACtD,UAAU,CAAC,EAAE,YAAY,CAAC;CAC7B,GAAG,MAAM,EAAE,CAYX;AAED,kFAAkF;AAClF,wBAAgB,aAAa,IAAI,MAAM,EAAE,CAExC;AAaD;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACrC,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,MAAM,EACd,SAAS,SAAS,GACnB,OAAO,CAAC,IAAI,CAAC,CAWf;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACnC,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,SAAS,SAAS,GACnB,OAAO,CAAC,MAAM,CAAC,CA0BjB;AAMD;;;;GAIG;AACH,wBAAsB,YAAY,CAC9B,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EAAE,EACd,gBAAgB,EAAE,MAAM,GACzB,OAAO,CAAC,MAAM,CAAC,CAwBjB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAoBrE;AAED,4FAA4F;AAC5F,wBAAgB,gBAAgB,CAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,KAAK,SAAO,GACb,MAAM,CAER"}
@@ -6,7 +6,7 @@
6
6
  * `ProcessExecutor.run` has no stdin input; fm's argv limit is far above the
7
7
  * token budget, and the pre-flight `count-tokens` check keeps it bounded.
8
8
  */
9
- import { DecisionBackendError, DecisionConfigError, DecisionRequestError, DecisionTimeoutError, } from '@gobing-ai/ts-ai-runner';
9
+ import { DecisionBackendError, DecisionConfigError, DecisionRequestError, DecisionTimeoutError, requireEnumValue as requireCoreEnumValue, } from '@gobing-ai/ts-ai-runner';
10
10
  /**
11
11
  * Subcommand + flag order verified against the design's live observations
12
12
  * (task 0086 R4): instructions ride in `--instructions=<value>` and the prompt
@@ -147,10 +147,6 @@ export function parseFmRespond(stdout) {
147
147
  return record;
148
148
  }
149
149
  /** A parsed value outside the declared enum is a schema violation, never an answer (R7). */
150
- export function requireEnumValue(parsed, key, allowed) {
151
- const value = parsed[key];
152
- if (value === undefined || !allowed.includes(value)) {
153
- throw new DecisionBackendError(`fm sample for '${key}' violates the schema enum: ${value === undefined ? '(missing)' : `'${value}'`}`, undefined);
154
- }
155
- return value;
150
+ export function requireEnumValue(parsed, key, allowed, label = 'fm') {
151
+ return requireCoreEnumValue(parsed, key, allowed, label);
156
152
  }
package/dist/schema.d.ts CHANGED
@@ -1,43 +1,7 @@
1
1
  /**
2
- * fm request construction (task 0084 R3, design § Request construction):
3
- * the JSON schema passed to `fm respond --schema` and the instruction/prompt
4
- * texts passed via `-i` and as the final positional argument.
5
- *
6
- * The schema always carries `x-order` — a hand-written object schema without it
7
- * is rejected by `fm` ("The data couldn't be read because it is missing").
8
- * Score levels are string enums so every question uses the same constrained
9
- * string path; the driver converts them back to numbers.
2
+ * fm request-construction aliases (task 0114 R8). The bodies moved to the
3
+ * dependency-free sampled-driver core in `@gobing-ai/ts-ai-runner`; the fm names
4
+ * stay exported so nothing that imported this module changes.
10
5
  */
11
- import type { DecisionState, Desc, Question } from '@gobing-ai/ts-ai-runner';
12
- /**
13
- * Fixed `-i` instructions. It never asks for confidence, certainty or a
14
- * probability — self-reported confidence would be fabricated calibration,
15
- * which the decision contract forbids (ADR-030).
16
- */
17
- export declare const FM_INSTRUCTIONS: string;
18
- /** The JSON schema object handed to `fm respond --schema` (R3). */
19
- export interface FmSchema {
20
- type: 'object';
21
- title: string;
22
- 'x-order': string[];
23
- required: string[];
24
- additionalProperties: false;
25
- properties: Record<string, {
26
- type: 'string';
27
- enum: string[];
28
- }>;
29
- }
30
- /** Build the fm schema over the whole question map: one call answers every key. */
31
- export declare function buildFmSchema(questions: Record<string, Question>): FmSchema;
32
- /** Format a {@link Desc} into prompt text: strings verbatim, structured values JSON, absence empty. */
33
- export declare function formatDesc(desc: Desc | undefined): string;
34
- /** The declared option values of a question, in declaration order (estimation + validation share this). */
35
- export declare function declaredOptions(question: Question): string[];
36
- /**
37
- * Render the positional prompt: the state as text (JSON when structured), then
38
- * one block per question key with the prompt `Desc` and its options with
39
- * descriptions — choice label keys, score level indices with the rubric text,
40
- * noul `yes`/`no` with any outcome descriptions.
41
- */
42
- export declare function buildPrompt(state: DecisionState, questions: Record<string, Question>): string;
6
+ export { buildEnumSchema as buildFmSchema, buildPrompt, DECISION_INSTRUCTIONS as FM_INSTRUCTIONS, declaredOptions, type EnumSchema as FmSchema, formatDesc, } from '@gobing-ai/ts-ai-runner';
43
7
  //# sourceMappingURL=schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAE7E;;;;GAIG;AACH,eAAO,MAAM,eAAe,QAGyB,CAAC;AAEtD,mEAAmE;AACnE,MAAM,WAAW,QAAQ;IACrB,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,oBAAoB,EAAE,KAAK,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CAClE;AAED,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAwB3E;AAED,uGAAuG;AACvG,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,GAAG,MAAM,CAIzD;AAED,2GAA2G;AAC3G,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,EAAE,CAS5D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,MAAM,CAkC7F"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACH,eAAe,IAAI,aAAa,EAChC,WAAW,EACX,qBAAqB,IAAI,eAAe,EACxC,eAAe,EACf,KAAK,UAAU,IAAI,QAAQ,EAC3B,UAAU,GACb,MAAM,yBAAyB,CAAC"}
package/dist/schema.js CHANGED
@@ -1,104 +1,6 @@
1
1
  /**
2
- * fm request construction (task 0084 R3, design § Request construction):
3
- * the JSON schema passed to `fm respond --schema` and the instruction/prompt
4
- * texts passed via `-i` and as the final positional argument.
5
- *
6
- * The schema always carries `x-order` — a hand-written object schema without it
7
- * is rejected by `fm` ("The data couldn't be read because it is missing").
8
- * Score levels are string enums so every question uses the same constrained
9
- * string path; the driver converts them back to numbers.
2
+ * fm request-construction aliases (task 0114 R8). The bodies moved to the
3
+ * dependency-free sampled-driver core in `@gobing-ai/ts-ai-runner`; the fm names
4
+ * stay exported so nothing that imported this module changes.
10
5
  */
11
- /**
12
- * Fixed `-i` instructions. It never asks for confidence, certainty or a
13
- * probability — self-reported confidence would be fabricated calibration,
14
- * which the decision contract forbids (ADR-030).
15
- */
16
- export const FM_INSTRUCTIONS = 'You are a decision engine. Answer every question below using only the supplied state. ' +
17
- 'For each question choose exactly one of the listed options. ' +
18
- 'Reply with the JSON object the schema requires.';
19
- /** Build the fm schema over the whole question map: one call answers every key. */
20
- export function buildFmSchema(questions) {
21
- const properties = {};
22
- for (const [key, question] of Object.entries(questions)) {
23
- switch (question.kind) {
24
- case 'choice':
25
- properties[key] = { type: 'string', enum: Object.keys(question.labels) };
26
- break;
27
- case 'score':
28
- properties[key] = { type: 'string', enum: question.rubric.map((_, level) => String(level)) };
29
- break;
30
- case 'noul':
31
- properties[key] = { type: 'string', enum: ['yes', 'no'] };
32
- break;
33
- }
34
- }
35
- const keys = Object.keys(questions);
36
- return {
37
- type: 'object',
38
- title: 'Decision',
39
- 'x-order': keys,
40
- required: keys,
41
- additionalProperties: false,
42
- properties,
43
- };
44
- }
45
- /** Format a {@link Desc} into prompt text: strings verbatim, structured values JSON, absence empty. */
46
- export function formatDesc(desc) {
47
- if (typeof desc === 'string')
48
- return desc;
49
- if (desc === null || desc === undefined)
50
- return '';
51
- return JSON.stringify(desc);
52
- }
53
- /** The declared option values of a question, in declaration order (estimation + validation share this). */
54
- export function declaredOptions(question) {
55
- switch (question.kind) {
56
- case 'choice':
57
- return Object.keys(question.labels);
58
- case 'score':
59
- return question.rubric.map((_, level) => String(level));
60
- case 'noul':
61
- return ['yes', 'no'];
62
- }
63
- }
64
- /**
65
- * Render the positional prompt: the state as text (JSON when structured), then
66
- * one block per question key with the prompt `Desc` and its options with
67
- * descriptions — choice label keys, score level indices with the rubric text,
68
- * noul `yes`/`no` with any outcome descriptions.
69
- */
70
- export function buildPrompt(state, questions) {
71
- const parts = [];
72
- const stateText = typeof state === 'string' ? state : state === null || state === undefined ? '' : JSON.stringify(state);
73
- if (stateText.length > 0)
74
- parts.push(stateText);
75
- for (const [key, question] of Object.entries(questions)) {
76
- const lines = [];
77
- const prompt = formatDesc(question.prompt);
78
- lines.push(`Question "${key}":${prompt.length > 0 ? ` ${prompt}` : ''}`);
79
- lines.push('Options:');
80
- switch (question.kind) {
81
- case 'choice':
82
- for (const [label, desc] of Object.entries(question.labels)) {
83
- const d = formatDesc(desc);
84
- lines.push(d.length > 0 ? `- ${label}: ${d}` : `- ${label}`);
85
- }
86
- break;
87
- case 'score':
88
- question.rubric.forEach((desc, level) => {
89
- const d = formatDesc(desc);
90
- lines.push(d.length > 0 ? `- ${level}: ${d}` : `- ${level}`);
91
- });
92
- break;
93
- case 'noul': {
94
- for (const outcome of ['yes', 'no']) {
95
- const d = formatDesc(question[outcome]);
96
- lines.push(d.length > 0 ? `- ${outcome}: ${d}` : `- ${outcome}`);
97
- }
98
- break;
99
- }
100
- }
101
- parts.push(lines.join('\n'));
102
- }
103
- return parts.join('\n\n');
104
- }
6
+ export { buildEnumSchema as buildFmSchema, buildPrompt, DECISION_INSTRUCTIONS as FM_INSTRUCTIONS, declaredOptions, formatDesc, } from '@gobing-ai/ts-ai-runner';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-decision-fm",
3
- "version": "0.5.30",
3
+ "version": "0.5.32",
4
4
  "description": "@gobing-ai/ts-decision-fm — Apple on-device Foundation Model decision backend driving the macOS `fm` CLI over a one-shot process bridge.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -51,8 +51,8 @@
51
51
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-decision-fm-v<version> && git push --tags' && exit 1"
52
52
  },
53
53
  "dependencies": {
54
- "@gobing-ai/ts-ai-runner": "^0.5.30",
55
- "@gobing-ai/ts-runtime": "^0.5.30"
54
+ "@gobing-ai/ts-ai-runner": "^0.5.32",
55
+ "@gobing-ai/ts-runtime": "^0.5.32"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/bun": "1.4.3"
package/src/driver.ts CHANGED
@@ -1,13 +1,17 @@
1
1
  /**
2
- * The Apple `fm` decision driver (task 0084 R2, R8, R9; design § Main export).
3
- * Satisfies the same {@link DecisionDriver} contract as the other backends over
4
- * one-shot `fm` processes: one schema per ask answers the whole question map,
5
- * probabilities come from k sample frequencies (ADR-030), and every failure
6
- * surfaces through the decision error taxonomy — never as an answer.
2
+ * The Apple `fm` decision driver (task 0084 R2, R8, R9; task 0114 R7). Satisfies
3
+ * the same {@link DecisionDriver} contract as the other backends over one-shot
4
+ * `fm` processes, built on the shared sampled-driver core: one schema per ask
5
+ * answers the whole question map, probabilities come from k sample frequencies
6
+ * (ADR-030), and every failure surfaces through the decision error taxonomy —
7
+ * never as an answer.
7
8
  */
8
9
 
9
10
  import {
10
11
  type Answer,
12
+ buildEnumSchema,
13
+ createSampledDriver,
14
+ DECISION_INSTRUCTIONS,
11
15
  DecisionConfigError,
12
16
  type DecisionDriver,
13
17
  DecisionRequestError,
@@ -21,17 +25,14 @@ import {
21
25
  nodeBunFactory,
22
26
  type ProcessExecutor,
23
27
  } from '@gobing-ai/ts-runtime';
24
- import { estimateChoice, estimateNoul, estimateScore } from './estimate';
25
28
  import {
26
29
  countPromptTokens,
27
30
  type FmGuardrails,
28
31
  parseFmRespond,
29
32
  probeFmAvailability,
30
- requireEnumValue,
31
33
  respondArgv,
32
34
  runFmRespond,
33
35
  } from './fm-process';
34
- import { buildFmSchema, buildPrompt, declaredOptions, FM_INSTRUCTIONS } from './schema';
35
36
 
36
37
  /** Configuration options for {@link createFmDriver}. */
37
38
  export interface FmDriverOptions {
@@ -80,10 +81,10 @@ export function createFmDriver(options: FmDriverOptions = {}): FmDecisionDriver
80
81
  arch = process.arch,
81
82
  } = options;
82
83
 
84
+ // Order kept from the pre-refactor driver: the samples error precedes PLATFORM.
83
85
  if (!Number.isInteger(samples) || samples < 1) {
84
86
  throw new DecisionRequestError(`samples must be a positive integer; got ${samples}`, undefined, undefined);
85
87
  }
86
- const k = deterministic ? 1 : samples;
87
88
 
88
89
  if (platform !== 'darwin' || arch !== 'arm64') {
89
90
  throw new DecisionConfigError(
@@ -101,53 +102,16 @@ export function createFmDriver(options: FmDriverOptions = {}): FmDecisionDriver
101
102
  availabilityVerified = true;
102
103
  }
103
104
 
104
- return {
105
+ const inner = createSampledDriver({
105
106
  name: 'fm-local',
106
- estimator: { kind: 'sample-frequency', samples: k, greedy: deterministic },
107
-
108
- async ask({
109
- state,
110
- questions,
111
- model,
112
- }: {
113
- state: DecisionState;
114
- questions: Record<string, Question>;
115
- model?: string;
116
- }): Promise<Record<string, Answer>> {
117
- // R9: only the on-device system model exists (design: fm CLI facts).
118
- if (model !== undefined && model !== 'system') {
119
- throw new DecisionRequestError(
120
- `fm-local supports only the 'system' model (or undefined); got '${model}'`,
121
- undefined,
122
- undefined,
123
- );
124
- }
125
- const entries = Object.entries(questions);
126
- if (entries.length === 0) {
127
- throw new DecisionRequestError('expected a nonempty question map', undefined, undefined);
128
- }
129
- for (const [key, question] of entries) {
130
- const optionCount =
131
- question.kind === 'choice'
132
- ? Object.keys(question.labels).length
133
- : question.kind === 'score'
134
- ? question.rubric.length
135
- : 2; // noul is binary by type
136
- if (optionCount < 2) {
137
- throw new DecisionRequestError(
138
- `question '${key}' has fewer than two options (${optionCount})`,
139
- undefined,
140
- undefined,
141
- );
142
- }
143
- }
144
-
107
+ sampleLabel: 'fm',
108
+ samples,
109
+ deterministic,
110
+ instructions: DECISION_INSTRUCTIONS,
111
+ async open({ instructions, prompt, questions }) {
145
112
  // R8: prerequisite probe before the first sample; cached per driver.
146
113
  await ensureFmAvailable();
147
114
 
148
- const instructions = FM_INSTRUCTIONS;
149
- const prompt = buildPrompt(state, questions);
150
-
151
115
  // R4: pre-flight budget — over budget rejects before any `fm respond`.
152
116
  const tokens = await countPromptTokens(executor, fmPath, instructions, prompt, requestTimeoutMs);
153
117
  if (tokens > maxPromptTokens) {
@@ -159,48 +123,56 @@ export function createFmDriver(options: FmDriverOptions = {}): FmDecisionDriver
159
123
  }
160
124
 
161
125
  // R3: one schema per ask over the whole question map, in a uniquely
162
- // named temp file, deleted in `finally` on success and failure.
126
+ // named temp file, deleted by `close` on success and failure.
163
127
  const schemaPath = joinPath(getProcessEnv().TMPDIR ?? '/tmp', `fm-schema-${crypto.randomUUID()}.json`);
164
- try {
165
- fs.writeFile(schemaPath, JSON.stringify(buildFmSchema(questions)));
166
-
167
- // R5: k sequential samples; each stdout parsed as JSON, read by key.
168
- const allowed = new Map(entries.map(([key, question]) => [key, declaredOptions(question)]));
169
- const samplesByKey = new Map(entries.map(([key]) => [key, [] as string[]]));
170
- for (let i = 0; i < k; i++) {
128
+ const session = {
129
+ async sample(): Promise<Record<string, string>> {
171
130
  const stdout = await runFmRespond(
172
131
  executor,
173
132
  fmPath,
174
133
  respondArgv({ schemaPath, instructions, prompt, greedy: deterministic, guardrails }),
175
134
  requestTimeoutMs,
176
135
  );
177
- const parsed = parseFmRespond(stdout);
178
- for (const [key, values] of samplesByKey) {
179
- values.push(requireEnumValue(parsed, key, allowed.get(key) ?? []));
180
- }
181
- }
182
-
183
- // R6: frequencies over the declared labels/levels, argmax with
184
- // declaration-order / lower-level tie-break, entropy confidence.
185
- const answers: Record<string, Answer> = {};
186
- for (const [key, question] of entries) {
187
- const values = samplesByKey.get(key) ?? [];
188
- switch (question.kind) {
189
- case 'choice':
190
- answers[key] = estimateChoice(Object.keys(question.labels), values);
191
- break;
192
- case 'score':
193
- answers[key] = estimateScore(question.rubric, values);
194
- break;
195
- case 'noul':
196
- answers[key] = estimateNoul(values);
197
- break;
198
- }
199
- }
200
- return answers;
201
- } finally {
136
+ return parseFmRespond(stdout);
137
+ },
138
+ close(): void {
139
+ fs.deleteFile(schemaPath);
140
+ },
141
+ };
142
+ try {
143
+ fs.writeFile(schemaPath, JSON.stringify(buildEnumSchema(questions)));
144
+ } catch (error) {
145
+ // Mirrors the pre-refactor `finally`: a failed write still deletes.
202
146
  fs.deleteFile(schemaPath);
147
+ throw error;
148
+ }
149
+ return session;
150
+ },
151
+ });
152
+
153
+ return {
154
+ name: 'fm-local',
155
+ estimator: inner.estimator,
156
+
157
+ // R9: only the on-device system model exists (design: fm CLI facts). The
158
+ // guard runs before the core so the model error keeps today's precedence.
159
+ async ask({
160
+ state,
161
+ questions,
162
+ model,
163
+ }: {
164
+ state: DecisionState;
165
+ questions: Record<string, Question>;
166
+ model?: string;
167
+ }): Promise<Record<string, Answer>> {
168
+ if (model !== undefined && model !== 'system') {
169
+ throw new DecisionRequestError(
170
+ `fm-local supports only the 'system' model (or undefined); got '${model}'`,
171
+ undefined,
172
+ undefined,
173
+ );
203
174
  }
175
+ return inner.ask({ state, questions, model });
204
176
  },
205
177
  };
206
178
  }
package/src/estimate.ts CHANGED
@@ -1,97 +1,7 @@
1
1
  /**
2
- * Probability estimation from repeated samples (task 0084 R6, ADR-030,
3
- * design § Probability estimation). Every probability is an empirical sample
4
- * frequency — never a self-reported confidence — so the resolution limit is
5
- * 1/k and `confidence` measures how much the k samples agreed, not calibration.
2
+ * Estimator aliases (task 0114 R8). The bodies moved to the dependency-free
3
+ * sampled-driver core in `@gobing-ai/ts-ai-runner`; the fm names stay exported
4
+ * so nothing that imported this module changes.
6
5
  */
7
- import type { ChoiceAnswer, Desc, NoulAnswer, ScoreAnswer } from '@gobing-ai/ts-ai-runner';
8
- /**
9
- * Laya reference `confidence_from_probs` on the empirical distribution:
10
- * `1 − H(p) / ln(n)` over the n declared options. One-hot ⇒ 1, uniform ⇒ 0.
11
- */
12
- export function entropyConfidence(probabilities: number[]): number {
13
- const n = probabilities.length;
14
- if (n <= 1) return 1;
15
- let entropy = 0;
16
- for (const p of probabilities) {
17
- if (p > 0) entropy -= p * Math.log(p);
18
- }
19
- return 1 - entropy / Math.log(n);
20
- }
21
-
22
- /**
23
- * Choice frequencies over every supplied label (unsampled ⇒ 0). The answer
24
- * label is the most frequent with ties broken by label declaration order.
25
- */
26
- export function estimateChoice(labels: readonly string[], samples: readonly string[]): ChoiceAnswer<string> {
27
- const counts = new Map<string, number>(labels.map((label) => [label, 0]));
28
- for (const sample of samples) counts.set(sample, (counts.get(sample) ?? 0) + 1);
29
- const k = samples.length;
30
- const probabilities: Record<string, number> = {};
31
- const distribution: number[] = [];
32
- let bestLabel = labels[0] ?? '';
33
- let bestCount = -1;
34
- for (const label of labels) {
35
- const count = counts.get(label) ?? 0;
36
- const probability = count / k;
37
- distribution.push(probability);
38
- probabilities[label] = probability;
39
- // Declaration order wins ties: strict `>` keeps the earlier label.
40
- if (count > bestCount) {
41
- bestCount = count;
42
- bestLabel = label;
43
- }
44
- }
45
- return {
46
- kind: 'choice',
47
- label: bestLabel,
48
- confidence: entropyConfidence(distribution),
49
- probabilities,
50
- };
51
- }
52
-
53
- /**
54
- * Score frequencies over the level indices `0…n-1`. The answer score is the
55
- * most frequent level with ties going to the lower level; the legend maps each
56
- * index back to its rubric description.
57
- */
58
- export function estimateScore(rubric: readonly Desc[], samples: readonly string[]): ScoreAnswer {
59
- const k = samples.length;
60
- const counts: number[] = [];
61
- for (const sample of samples) {
62
- const level = Number.parseInt(sample, 10);
63
- counts[level] = (counts[level] ?? 0) + 1;
64
- }
65
- const probabilities: Record<number, number> = {};
66
- const distribution: number[] = [];
67
- let bestLevel = 0;
68
- let bestCount = -1;
69
- for (let level = 0; level < rubric.length; level++) {
70
- const count = counts[level] ?? 0;
71
- const probability = count / k;
72
- distribution.push(probability);
73
- probabilities[level] = probability;
74
- // Lower level wins ties: strict `>` keeps the smaller index.
75
- if (count > bestCount) {
76
- bestCount = count;
77
- bestLevel = level;
78
- }
79
- }
80
- const legend: Record<number, Desc> = {};
81
- rubric.forEach((desc, level) => {
82
- legend[level] = desc;
83
- });
84
- return {
85
- kind: 'score',
86
- score: bestLevel,
87
- confidence: entropyConfidence(distribution),
88
- legend,
89
- probabilities,
90
- };
91
- }
92
6
 
93
- /** Noul is a bare yes-probability — no `confidence` key, by contract. */
94
- export function estimateNoul(samples: readonly string[]): NoulAnswer {
95
- const yes = samples.filter((sample) => sample === 'yes').length;
96
- return { kind: 'noul', probability: yes / samples.length };
97
- }
7
+ export { entropyConfidence, estimateChoice, estimateNoul, estimateScore } from '@gobing-ai/ts-ai-runner';
package/src/fm-process.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  DecisionConfigError,
13
13
  DecisionRequestError,
14
14
  DecisionTimeoutError,
15
+ requireEnumValue as requireCoreEnumValue,
15
16
  } from '@gobing-ai/ts-ai-runner';
16
17
  import type { ProcessExecutor, ProcessResult } from '@gobing-ai/ts-runtime';
17
18
 
@@ -199,13 +200,11 @@ export function parseFmRespond(stdout: string): Record<string, string> {
199
200
  }
200
201
 
201
202
  /** A parsed value outside the declared enum is a schema violation, never an answer (R7). */
202
- export function requireEnumValue(parsed: Record<string, string>, key: string, allowed: readonly string[]): string {
203
- const value = parsed[key];
204
- if (value === undefined || !allowed.includes(value)) {
205
- throw new DecisionBackendError(
206
- `fm sample for '${key}' violates the schema enum: ${value === undefined ? '(missing)' : `'${value}'`}`,
207
- undefined,
208
- );
209
- }
210
- return value;
203
+ export function requireEnumValue(
204
+ parsed: Record<string, string>,
205
+ key: string,
206
+ allowed: readonly string[],
207
+ label = 'fm',
208
+ ): string {
209
+ return requireCoreEnumValue(parsed, key, allowed, label);
211
210
  }
package/src/schema.ts CHANGED
@@ -1,120 +1,14 @@
1
1
  /**
2
- * fm request construction (task 0084 R3, design § Request construction):
3
- * the JSON schema passed to `fm respond --schema` and the instruction/prompt
4
- * texts passed via `-i` and as the final positional argument.
5
- *
6
- * The schema always carries `x-order` — a hand-written object schema without it
7
- * is rejected by `fm` ("The data couldn't be read because it is missing").
8
- * Score levels are string enums so every question uses the same constrained
9
- * string path; the driver converts them back to numbers.
2
+ * fm request-construction aliases (task 0114 R8). The bodies moved to the
3
+ * dependency-free sampled-driver core in `@gobing-ai/ts-ai-runner`; the fm names
4
+ * stay exported so nothing that imported this module changes.
10
5
  */
11
6
 
12
- import type { DecisionState, Desc, Question } from '@gobing-ai/ts-ai-runner';
13
-
14
- /**
15
- * Fixed `-i` instructions. It never asks for confidence, certainty or a
16
- * probability — self-reported confidence would be fabricated calibration,
17
- * which the decision contract forbids (ADR-030).
18
- */
19
- export const FM_INSTRUCTIONS =
20
- 'You are a decision engine. Answer every question below using only the supplied state. ' +
21
- 'For each question choose exactly one of the listed options. ' +
22
- 'Reply with the JSON object the schema requires.';
23
-
24
- /** The JSON schema object handed to `fm respond --schema` (R3). */
25
- export interface FmSchema {
26
- type: 'object';
27
- title: string;
28
- 'x-order': string[];
29
- required: string[];
30
- additionalProperties: false;
31
- properties: Record<string, { type: 'string'; enum: string[] }>;
32
- }
33
-
34
- /** Build the fm schema over the whole question map: one call answers every key. */
35
- export function buildFmSchema(questions: Record<string, Question>): FmSchema {
36
- const properties: Record<string, { type: 'string'; enum: string[] }> = {};
37
- for (const [key, question] of Object.entries(questions)) {
38
- switch (question.kind) {
39
- case 'choice':
40
- properties[key] = { type: 'string', enum: Object.keys(question.labels) };
41
- break;
42
- case 'score':
43
- properties[key] = { type: 'string', enum: question.rubric.map((_, level) => String(level)) };
44
- break;
45
- case 'noul':
46
- properties[key] = { type: 'string', enum: ['yes', 'no'] };
47
- break;
48
- }
49
- }
50
- const keys = Object.keys(questions);
51
- return {
52
- type: 'object',
53
- title: 'Decision',
54
- 'x-order': keys,
55
- required: keys,
56
- additionalProperties: false,
57
- properties,
58
- };
59
- }
60
-
61
- /** Format a {@link Desc} into prompt text: strings verbatim, structured values JSON, absence empty. */
62
- export function formatDesc(desc: Desc | undefined): string {
63
- if (typeof desc === 'string') return desc;
64
- if (desc === null || desc === undefined) return '';
65
- return JSON.stringify(desc);
66
- }
67
-
68
- /** The declared option values of a question, in declaration order (estimation + validation share this). */
69
- export function declaredOptions(question: Question): string[] {
70
- switch (question.kind) {
71
- case 'choice':
72
- return Object.keys(question.labels);
73
- case 'score':
74
- return question.rubric.map((_, level) => String(level));
75
- case 'noul':
76
- return ['yes', 'no'];
77
- }
78
- }
79
-
80
- /**
81
- * Render the positional prompt: the state as text (JSON when structured), then
82
- * one block per question key with the prompt `Desc` and its options with
83
- * descriptions — choice label keys, score level indices with the rubric text,
84
- * noul `yes`/`no` with any outcome descriptions.
85
- */
86
- export function buildPrompt(state: DecisionState, questions: Record<string, Question>): string {
87
- const parts: string[] = [];
88
- const stateText =
89
- typeof state === 'string' ? state : state === null || state === undefined ? '' : JSON.stringify(state);
90
- if (stateText.length > 0) parts.push(stateText);
91
- for (const [key, question] of Object.entries(questions)) {
92
- const lines: string[] = [];
93
- const prompt = formatDesc(question.prompt);
94
- lines.push(`Question "${key}":${prompt.length > 0 ? ` ${prompt}` : ''}`);
95
- lines.push('Options:');
96
- switch (question.kind) {
97
- case 'choice':
98
- for (const [label, desc] of Object.entries(question.labels)) {
99
- const d = formatDesc(desc);
100
- lines.push(d.length > 0 ? `- ${label}: ${d}` : `- ${label}`);
101
- }
102
- break;
103
- case 'score':
104
- question.rubric.forEach((desc, level) => {
105
- const d = formatDesc(desc);
106
- lines.push(d.length > 0 ? `- ${level}: ${d}` : `- ${level}`);
107
- });
108
- break;
109
- case 'noul': {
110
- for (const outcome of ['yes', 'no'] as const) {
111
- const d = formatDesc(question[outcome]);
112
- lines.push(d.length > 0 ? `- ${outcome}: ${d}` : `- ${outcome}`);
113
- }
114
- break;
115
- }
116
- }
117
- parts.push(lines.join('\n'));
118
- }
119
- return parts.join('\n\n');
120
- }
7
+ export {
8
+ buildEnumSchema as buildFmSchema,
9
+ buildPrompt,
10
+ DECISION_INSTRUCTIONS as FM_INSTRUCTIONS,
11
+ declaredOptions,
12
+ type EnumSchema as FmSchema,
13
+ formatDesc,
14
+ } from '@gobing-ai/ts-ai-runner';