@itookit/dsht 0.5.1 → 0.6.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 (93) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +10 -4
  3. package/README.zh.md +12 -6
  4. package/dist/catalog/controller.d.ts +26 -6
  5. package/dist/catalog/controller.js +73 -45
  6. package/dist/catalog/index.d.ts +1 -0
  7. package/dist/cli/dsht.js +22 -2
  8. package/dist/cli/startup.js +30 -11
  9. package/dist/cli/verifier.d.ts +4 -0
  10. package/dist/cli/verifier.js +28 -5
  11. package/dist/contracts.d.ts +42 -5
  12. package/dist/controller/connection-streams.d.ts +22 -0
  13. package/dist/controller/connection-streams.js +105 -0
  14. package/dist/controller/connection.d.ts +14 -3
  15. package/dist/controller/connection.js +40 -69
  16. package/dist/controller/controller.d.ts +20 -234
  17. package/dist/controller/controller.js +113 -811
  18. package/dist/controller/foreground.d.ts +44 -0
  19. package/dist/controller/foreground.js +79 -0
  20. package/dist/controller/loop-coordinator.d.ts +48 -0
  21. package/dist/controller/loop-coordinator.js +647 -0
  22. package/dist/controller/loop-prompts-schema.d.ts +16 -2
  23. package/dist/controller/loop-prompts-schema.js +106 -27
  24. package/dist/controller/loop-prompts.d.ts +17 -2
  25. package/dist/controller/loop-prompts.generated.js +2 -1
  26. package/dist/controller/loop-prompts.js +35 -9
  27. package/dist/controller/loop-protocols.d.ts +3 -1
  28. package/dist/controller/loop-protocols.js +8 -3
  29. package/dist/controller/loop-source.d.ts +74 -0
  30. package/dist/controller/loop-source.js +224 -0
  31. package/dist/controller/verifier.d.ts +4 -0
  32. package/dist/cost/controller.d.ts +3 -1
  33. package/dist/cost/controller.js +26 -7
  34. package/dist/cost/index.d.ts +1 -1
  35. package/dist/cost/index.js +1 -1
  36. package/dist/cost/ledger-files.d.ts +20 -0
  37. package/dist/cost/ledger-files.js +115 -15
  38. package/dist/cost/ledger.d.ts +31 -6
  39. package/dist/cost/ledger.js +74 -22
  40. package/dist/cost/pricing.d.ts +39 -0
  41. package/dist/cost/pricing.js +46 -0
  42. package/dist/cost/scanner.js +1 -0
  43. package/dist/cost/types.d.ts +9 -3
  44. package/dist/session/controller.d.ts +23 -35
  45. package/dist/session/controller.js +113 -363
  46. package/dist/session/history-reader.d.ts +32 -0
  47. package/dist/session/history-reader.js +170 -0
  48. package/dist/session/index.d.ts +1 -1
  49. package/dist/session/info.d.ts +3 -38
  50. package/dist/session/info.js +14 -1
  51. package/dist/session/interactions.d.ts +26 -0
  52. package/dist/session/interactions.js +75 -0
  53. package/dist/session/navigator.d.ts +47 -0
  54. package/dist/session/navigator.js +158 -0
  55. package/dist/session/prompt-backfill.d.ts +23 -0
  56. package/dist/session/prompt-backfill.js +88 -0
  57. package/dist/session/state.d.ts +20 -0
  58. package/dist/session/state.js +1 -0
  59. package/dist/session/telemetry.d.ts +15 -6
  60. package/dist/session/telemetry.js +44 -7
  61. package/dist/session/transcript.d.ts +5 -1
  62. package/dist/slash/index.d.ts +1 -1
  63. package/dist/slash/parse.d.ts +2 -126
  64. package/dist/slash/registry.d.ts +1 -1
  65. package/dist/slash/types.d.ts +126 -0
  66. package/dist/slash/types.js +1 -0
  67. package/dist/state.d.ts +5 -17
  68. package/dist/state.js +1 -1
  69. package/dist/storage/files.d.ts +8 -0
  70. package/dist/storage/files.js +18 -1
  71. package/dist/storage/index.d.ts +1 -1
  72. package/dist/storage/index.js +1 -1
  73. package/dist/transport/client.d.ts +4 -3
  74. package/dist/transport/client.js +71 -25
  75. package/dist/ui/app.js +88 -301
  76. package/dist/ui/chat/shell-view.d.ts +2 -0
  77. package/dist/ui/chat/shell-view.js +8 -0
  78. package/dist/ui/chat/use-history-view.d.ts +69 -0
  79. package/dist/ui/chat/use-history-view.js +123 -0
  80. package/dist/ui/dialogs/cost.d.ts +6 -0
  81. package/dist/ui/dialogs/cost.js +5 -1
  82. package/dist/ui/dialogs/loop.d.ts +5 -4
  83. package/dist/ui/dialogs/loop.js +14 -6
  84. package/dist/ui/dialogs/use-panels.d.ts +53 -0
  85. package/dist/ui/dialogs/use-panels.js +51 -0
  86. package/dist/ui/input/use-composer.d.ts +35 -0
  87. package/dist/ui/input/use-composer.js +109 -0
  88. package/dist/ui/input/use-deferred-lines.d.ts +16 -0
  89. package/dist/ui/input/use-deferred-lines.js +54 -0
  90. package/dist/ui/input/use-history-recall.d.ts +20 -0
  91. package/dist/ui/input/use-history-recall.js +47 -0
  92. package/loop.yaml +230 -0
  93. package/package.json +5 -4
@@ -2,7 +2,11 @@
2
2
  *
3
3
  * Kept apart from `loop-prompts.ts` so the build script can validate a YAML file before the
4
4
  * generated module exists: this module imports nothing, while the renderer imports the generated
5
- * data. The validator is the single copy of the schema — the generator and the tests both call it.
5
+ * data. The validator is the single copy of the schema — the generator, the runtime loader and the
6
+ * tests all call it, so a file that passes here is the one the renderer will read.
7
+ *
8
+ * Two entry points share one set of rules: `validateLoopPrompts` checks a whole built-in file, and
9
+ * `validateLoopOverlayPrompts` checks a user file that may carry only the records it changes.
6
10
  */
7
11
  /** Placeholders every template may use; a record's own `vars` add to these. */
8
12
  export const LOOP_PLACEHOLDERS = ['from', 'to', 'score', 'tries', 'step', 'attempt', 'title', 'artifact', 'checks'];
@@ -16,18 +20,94 @@ export const RESERVED_PROTOCOL_NAMES = ['answer', 'abort', 'stop'];
16
20
  */
17
21
  export function validateLoopPrompts(source) {
18
22
  const errors = [];
19
- const document = source;
20
- if (document?.version !== 1)
23
+ const document = asDocument(source);
24
+ if (document.version !== 1)
21
25
  errors.push('version must be 1');
22
- if (!Number.isFinite(document?.defaults?.score))
23
- errors.push('defaults.score must be a number');
24
- if (!Number.isFinite(document?.defaults?.tries))
25
- errors.push('defaults.tries must be a number');
26
- const protocols = document?.protocols;
27
- if (protocols === null || typeof protocols !== 'object' || Object.keys(protocols ?? {}).length === 0) {
26
+ validateDefaults(document.defaults, errors, true);
27
+ const protocols = document.protocols;
28
+ if (protocols === null || typeof protocols !== 'object' || Array.isArray(protocols) || Object.keys(protocols ?? {}).length === 0) {
28
29
  errors.push('protocols must be a non-empty mapping');
29
30
  return errors;
30
31
  }
32
+ validateProtocols(protocols, errors);
33
+ return errors;
34
+ }
35
+ /** Check a user file that layers over the shipped records.
36
+ *
37
+ * It is a whole `loop.yaml` in shape, but partial in content: an overlay that only adds one record
38
+ * or only moves the global defaults is exactly what it is for, so a missing or empty `protocols` is
39
+ * valid here. Every record it does declare is held to the same rules as a shipped one, because the
40
+ * renderer cannot tell the two apart once they are merged.
41
+ * @param source - Parsed overlay file.
42
+ * @returns One message per problem; an empty list means valid.
43
+ */
44
+ export function validateLoopOverlayPrompts(source) {
45
+ const errors = [];
46
+ const document = asDocument(source);
47
+ // A version is required even from an overlay: a file written for a later shape must fail loudly
48
+ // instead of having its fields read as this shape's, which is how a silent misread becomes a run
49
+ // judged against a rubric nobody wrote.
50
+ if (document.version !== 1)
51
+ errors.push('version must be 1');
52
+ validateDefaults(document.defaults, errors, false);
53
+ const protocols = document.protocols;
54
+ if (protocols === undefined || protocols === null)
55
+ return errors;
56
+ if (typeof protocols !== 'object' || Array.isArray(protocols)) {
57
+ errors.push('protocols must be a mapping');
58
+ return errors;
59
+ }
60
+ validateProtocols(protocols, errors);
61
+ return errors;
62
+ }
63
+ /** A parsed document with every unsafe value normalized to `undefined`.
64
+ * @param source - Anything a caller parsed out of a file.
65
+ * @returns The document, or an empty one for a null, scalar or array root.
66
+ */
67
+ function asDocument(source) {
68
+ return source !== null && typeof source === 'object' && !Array.isArray(source) ? source : {};
69
+ }
70
+ /** Check the global defaults.
71
+ *
72
+ * A shipped file must state them, because every record falls back to them. An overlay may state
73
+ * neither, one or both, but a value it does state reaches real runs, so it is checked for range
74
+ * even though the shipped file only has to be numeric.
75
+ * @param defaults - The `defaults` field as parsed.
76
+ * @param errors - Collector to append messages to.
77
+ * @param required - Whether this document must carry both numbers.
78
+ */
79
+ function validateDefaults(defaults, errors, required) {
80
+ if (defaults === undefined || defaults === null) {
81
+ if (required) {
82
+ errors.push('defaults.score must be a number');
83
+ errors.push('defaults.tries must be a number');
84
+ }
85
+ return;
86
+ }
87
+ if (typeof defaults !== 'object' || Array.isArray(defaults)) {
88
+ errors.push('defaults must be a mapping');
89
+ return;
90
+ }
91
+ const { score, tries } = defaults;
92
+ if (required) {
93
+ if (!Number.isFinite(score))
94
+ errors.push('defaults.score must be a number');
95
+ if (!Number.isFinite(tries))
96
+ errors.push('defaults.tries must be a number');
97
+ return;
98
+ }
99
+ if (score !== undefined && (!Number.isFinite(score) || score < 0 || score > 10)) {
100
+ errors.push('defaults.score must be a number in 0-10');
101
+ }
102
+ if (tries !== undefined && (!Number.isInteger(tries) || tries < 1)) {
103
+ errors.push('defaults.tries must be a positive integer');
104
+ }
105
+ }
106
+ /** Check every record of one document against the rules the renderer relies on.
107
+ * @param protocols - The `protocols` mapping.
108
+ * @param errors - Collector to append messages to.
109
+ */
110
+ function validateProtocols(protocols, errors) {
31
111
  const runtime = new Set(LOOP_PLACEHOLDERS);
32
112
  const reserved = new Set(RESERVED_PROTOCOL_NAMES);
33
113
  const placeholders = (text) => [...text.matchAll(/\{\{(\w+)\}\}/g)].map(match => match[1]);
@@ -36,7 +116,7 @@ export function validateLoopPrompts(source) {
36
116
  if (reserved.has(kind))
37
117
  errors.push(`${at} is a reserved name: ${RESERVED_PROTOCOL_NAMES.join(', ')} belong to /loop itself`);
38
118
  // A template may name a runtime value or one of this record's own vars, and nothing else.
39
- const vars = protocol.vars;
119
+ const vars = protocol?.vars;
40
120
  if (vars !== undefined && (vars === null || typeof vars !== 'object' || Array.isArray(vars))) {
41
121
  errors.push(`${at}.vars must be a mapping of names to strings`);
42
122
  }
@@ -55,15 +135,15 @@ export function validateLoopPrompts(source) {
55
135
  if (!allowed.has(name))
56
136
  errors.push(`${where}: unknown placeholder {{${name}}}`);
57
137
  };
58
- checkPlaceholders(typeof protocol.title === 'string' ? protocol.title : '', `${at}.title`);
59
- if (typeof protocol.title !== 'string' || protocol.title === '')
138
+ checkPlaceholders(typeof protocol?.title === 'string' ? protocol.title : '', `${at}.title`);
139
+ if (typeof protocol?.title !== 'string' || protocol.title === '')
60
140
  errors.push(`${at}.title must be a non-empty string`);
61
- if (!Number.isInteger(protocol.steps) || (protocol.steps ?? 0) < 1)
141
+ if (!Number.isInteger(protocol?.steps) || (protocol?.steps ?? 0) < 1)
62
142
  errors.push(`${at}.steps must be a positive integer`);
63
- if (protocol.artifact !== undefined && (typeof protocol.artifact !== 'string' || protocol.artifact === '')) {
143
+ if (protocol?.artifact !== undefined && (typeof protocol.artifact !== 'string' || protocol.artifact === '')) {
64
144
  errors.push(`${at}.artifact must be a non-empty string when present`);
65
145
  }
66
- else if (typeof protocol.artifact === 'string') {
146
+ else if (typeof protocol?.artifact === 'string') {
67
147
  // The file name may use the record's vars — that is how one run per input gets its own file —
68
148
  // but never a runtime placeholder: the artifact must not move between rounds.
69
149
  for (const name of placeholders(protocol.artifact)) {
@@ -72,7 +152,7 @@ export function validateLoopPrompts(source) {
72
152
  }
73
153
  }
74
154
  }
75
- if (protocol.artifactMarker !== undefined) {
155
+ if (protocol?.artifactMarker !== undefined) {
76
156
  if (typeof protocol.artifactMarker !== 'string' || protocol.artifactMarker.trim() === '') {
77
157
  errors.push(`${at}.artifactMarker must be a non-empty string when present`);
78
158
  }
@@ -84,28 +164,28 @@ export function validateLoopPrompts(source) {
84
164
  errors.push(`${at}.artifactMarker needs ${at}.artifact: there is no file to look in`);
85
165
  }
86
166
  }
87
- if (typeof protocol.fallbackLabel !== 'string' || protocol.fallbackLabel === '')
167
+ if (typeof protocol?.fallbackLabel !== 'string' || protocol.fallbackLabel === '')
88
168
  errors.push(`${at}.fallbackLabel must be a non-empty string`);
89
- if (protocol.standard !== undefined && (typeof protocol.standard !== 'string' || protocol.standard.trim() === '')) {
169
+ if (protocol?.standard !== undefined && (typeof protocol.standard !== 'string' || protocol.standard.trim() === '')) {
90
170
  errors.push(`${at}.standard must be a non-empty string when present`);
91
171
  }
92
- if (protocol.starts !== undefined && protocol.starts !== 'verify' && protocol.starts !== 'work') {
172
+ if (protocol?.starts !== undefined && protocol.starts !== 'verify' && protocol.starts !== 'work') {
93
173
  errors.push(`${at}.starts must be 'verify' or 'work'`);
94
174
  }
95
- if (protocol.defaults !== undefined) {
175
+ if (protocol?.defaults !== undefined) {
96
176
  const { score, tries } = protocol.defaults;
97
177
  if (score !== undefined && (!Number.isFinite(score) || score < 0 || score > 10))
98
178
  errors.push(`${at}.defaults.score must be a number in 0-10`);
99
179
  if (tries !== undefined && (!Number.isInteger(tries) || tries < 1))
100
180
  errors.push(`${at}.defaults.tries must be a positive integer`);
101
181
  }
102
- const rounds = protocol.rounds;
182
+ const rounds = protocol?.rounds;
103
183
  if (!Array.isArray(rounds)) {
104
184
  errors.push(`${at}.rounds must be a list`);
105
185
  }
106
186
  else {
107
- if (rounds.length > 0 && rounds.length !== protocol.steps) {
108
- errors.push(`${at}.rounds has ${rounds.length} entries but steps is ${protocol.steps}`);
187
+ if (rounds.length > 0 && rounds.length !== protocol?.steps) {
188
+ errors.push(`${at}.rounds has ${rounds.length} entries but steps is ${protocol?.steps}`);
109
189
  }
110
190
  rounds.forEach((round, index) => {
111
191
  if (typeof round?.title !== 'string' || round.title === '')
@@ -115,7 +195,7 @@ export function validateLoopPrompts(source) {
115
195
  });
116
196
  }
117
197
  for (const key of ['brief', 'followUp']) {
118
- const lines = protocol[key];
198
+ const lines = protocol?.[key];
119
199
  if (!Array.isArray(lines) || lines.length === 0) {
120
200
  errors.push(`${at}.${key} must be a non-empty list`);
121
201
  continue;
@@ -128,17 +208,16 @@ export function validateLoopPrompts(source) {
128
208
  checkPlaceholders(line, `${at}.${key}[${index}]`);
129
209
  });
130
210
  }
131
- const checksLines = (protocol.brief ?? []).filter(line => typeof line === 'string' && line.trim() === '{{checks}}');
211
+ const checksLines = (protocol?.brief ?? []).filter(line => typeof line === 'string' && line.trim() === '{{checks}}');
132
212
  if ((rounds?.length ?? 0) > 0 && checksLines.length !== 1)
133
213
  errors.push(`${at}.brief must contain exactly one line that is just {{checks}}`);
134
214
  if ((rounds?.length ?? 0) === 0 && checksLines.length > 0)
135
215
  errors.push(`${at}.brief uses {{checks}} but defines no rounds`);
136
- if (protocol.verifyFocus !== undefined) {
216
+ if (protocol?.verifyFocus !== undefined) {
137
217
  if (typeof protocol.verifyFocus !== 'string' || protocol.verifyFocus === '')
138
218
  errors.push(`${at}.verifyFocus must be a non-empty string when present`);
139
219
  else
140
220
  checkPlaceholders(protocol.verifyFocus, `${at}.verifyFocus`);
141
221
  }
142
222
  }
143
- return errors;
144
223
  }
@@ -1,3 +1,5 @@
1
+ import type { LoopSourceInfo } from '../contracts.ts';
2
+ import type { LoopPromptSource } from './loop-prompts-schema.ts';
1
3
  export type { LoopPromptSource, LoopProtocolText, LoopRoundText } from './loop-prompts-schema.ts';
2
4
  /** Values one render call supplies; a record's `vars` are merged in on top of these. */
3
5
  export interface LoopPromptValues {
@@ -49,7 +51,20 @@ export interface LoopPrompts {
49
51
  /** One record's declared `vars`, unrendered, so a form can offer them before a run starts. */
50
52
  vars(kind: string): Readonly<Record<string, string>>;
51
53
  }
52
- /** The records of loop.yaml.
53
- * @returns Names, lookups and the declared vars of each record, built once.
54
+ /** Put the records this process runs in place, before any run or record list reads them.
55
+ *
56
+ * The files are read and merged at startup rather than at module load, so a bad user file can be
57
+ * reported and refused before the client opens, and a run keeps the rubric it started with even if
58
+ * the file is edited underneath it.
59
+ * @param source - Merged records: shipped ones with the user's file layered on top.
60
+ * @param sourceInfo - Where they came from, and what the user's file changed.
61
+ */
62
+ export declare function installLoopSource(source: LoopPromptSource, sourceInfo: LoopSourceInfo): void;
63
+ /** Where the installed records came from.
64
+ * @returns The installed source info; the compiled-in records are the empty default.
65
+ */
66
+ export declare function loopSourceInfo(): LoopSourceInfo;
67
+ /** The records in force.
68
+ * @returns Names, lookups and the declared vars of each record, built once per installed source.
54
69
  */
55
70
  export declare function loopPrompts(): LoopPrompts;
@@ -1,5 +1,6 @@
1
1
  // GENERATED FILE — do not edit. Edit loop.yaml and run `npm run build:prompts`.
2
- // Kept in sync by tests/controller/loop-prompts.test.ts.
2
+ // The fallback table: loop.yaml is read at runtime, and this is what a package whose file is
3
+ // missing or unreadable starts on. Kept in sync by tests/controller/loop-prompts.test.ts.
3
4
  export const LOOP_PROMPTS = {
4
5
  "version": 1,
5
6
  "defaults": {
@@ -1,14 +1,19 @@
1
1
  /** Typed access to the loop prompts and the placeholder renderer.
2
2
  *
3
- * `loop.yaml` is the editable source; `loop-prompts.generated.ts` is its inlined form and
4
- * `loop-prompts-schema.ts` holds the shape and the rules. This module turns one record into the
5
- * strings a protocol needs, so the dynamic parts (this round's title, its checklist, the record's
6
- * own vars) are filled here and nowhere else. A template may only use placeholders the caller can
7
- * supply; anything else throws rather than putting a literal `{{name}}` into a prompt.
3
+ * The loop record is configuration, not code: the shipped `loop.yaml` is read at startup from beside
4
+ * the package and a user file may be layered over it (`loop-source.ts`), then the merged table is
5
+ * installed here once. `loop-prompts-schema.ts` holds the shape and the rules, and
6
+ * `loop-prompts.generated.ts` is the compiled-in fallback for a package whose file is missing. This
7
+ * module turns one record into the strings a protocol needs, so the dynamic parts (this round's title,
8
+ * its checklist, the record's own vars) are filled here and nowhere else. A template may only use
9
+ * placeholders the caller can supply; anything else throws rather than putting a literal `{{name}}`
10
+ * into a prompt.
8
11
  */
9
12
  import { LOOP_PROMPTS } from "./loop-prompts.generated.js";
10
13
  const PLACEHOLDER = /\{\{(\w+)\}\}/g;
11
- const SOURCE = LOOP_PROMPTS;
14
+ /** The records in force: the shipped file with the user's layered on top, installed once at startup
15
+ * and replaced only by another install, never by a reload while a run is in flight. */
16
+ let SOURCE = LOOP_PROMPTS;
12
17
  /** Replace every `{{name}}` in one template list.
13
18
  * @param lines - Template lines.
14
19
  * @param values - Values keyed by placeholder name.
@@ -75,10 +80,31 @@ function render(kind, protocol, overrides) {
75
80
  followUp: runtime => fill(protocol.followUp, values(runtime), kind),
76
81
  };
77
82
  }
78
- /** All records, rendered once at module load: the config cannot change under a running client. */
83
+ /** All records, rendered when the source is installed: a file edited mid-run cannot change a brief. */
79
84
  let cache;
80
- /** The records of loop.yaml.
81
- * @returns Names, lookups and the declared vars of each record, built once.
85
+ /** Where the installed records came from, for the record list to show. */
86
+ let info = { overridden: [], added: [], warnings: [] };
87
+ /** Put the records this process runs in place, before any run or record list reads them.
88
+ *
89
+ * The files are read and merged at startup rather than at module load, so a bad user file can be
90
+ * reported and refused before the client opens, and a run keeps the rubric it started with even if
91
+ * the file is edited underneath it.
92
+ * @param source - Merged records: shipped ones with the user's file layered on top.
93
+ * @param sourceInfo - Where they came from, and what the user's file changed.
94
+ */
95
+ export function installLoopSource(source, sourceInfo) {
96
+ SOURCE = source;
97
+ info = { ...sourceInfo, overridden: [...sourceInfo.overridden], added: [...sourceInfo.added], warnings: [...sourceInfo.warnings] };
98
+ cache = undefined;
99
+ }
100
+ /** Where the installed records came from.
101
+ * @returns The installed source info; the compiled-in records are the empty default.
102
+ */
103
+ export function loopSourceInfo() {
104
+ return info;
105
+ }
106
+ /** The records in force.
107
+ * @returns Names, lookups and the declared vars of each record, built once per installed source.
82
108
  */
83
109
  export function loopPrompts() {
84
110
  if (cache !== undefined)
@@ -5,7 +5,9 @@ export declare function loopProtocolNames(): string[];
5
5
  /** Every record `/loop` may run, in file order, with the defaults a run would start from.
6
6
  *
7
7
  * The list and the run read the same records, so a chooser can show exactly the name, round count,
8
- * artifact and defaults the runner would use — never a second table that could drift.
8
+ * artifact and defaults the runner would use — never a second table that could drift. A record the
9
+ * user's own file supplied is marked, because an operator who overrode a shipped record can no longer
10
+ * tell the two apart from the name alone.
9
11
  * @returns One summary per record.
10
12
  */
11
13
  export declare function loopRecords(): LoopRecord[];
@@ -6,7 +6,7 @@
6
6
  * record without code knowing which one it is.
7
7
  */
8
8
  import { LOOP_MARKER, followUpContract, resultContract, verdictBrief } from "./loop-contract.js";
9
- import { loopPrompts } from "./loop-prompts.js";
9
+ import { loopPrompts, loopSourceInfo } from "./loop-prompts.js";
10
10
  import { coversWholeProtocol } from "./loop.js";
11
11
  /** Names `/loop` may run, in file order. */
12
12
  export function loopProtocolNames() {
@@ -15,10 +15,14 @@ export function loopProtocolNames() {
15
15
  /** Every record `/loop` may run, in file order, with the defaults a run would start from.
16
16
  *
17
17
  * The list and the run read the same records, so a chooser can show exactly the name, round count,
18
- * artifact and defaults the runner would use — never a second table that could drift.
18
+ * artifact and defaults the runner would use — never a second table that could drift. A record the
19
+ * user's own file supplied is marked, because an operator who overrode a shipped record can no longer
20
+ * tell the two apart from the name alone.
19
21
  * @returns One summary per record.
20
22
  */
21
23
  export function loopRecords() {
24
+ const { overridden, added } = loopSourceInfo();
25
+ const mine = new Set([...overridden, ...added]);
22
26
  return loopPrompts().names.flatMap(name => {
23
27
  const text = loopPrompts().find(name);
24
28
  if (text === undefined)
@@ -26,7 +30,8 @@ export function loopRecords() {
26
30
  return [{ name, title: text.title, steps: text.steps,
27
31
  ...(text.artifact === undefined ? {} : { artifact: text.artifact }),
28
32
  defaultScore: text.defaultScore, defaultTries: text.defaultTries,
29
- vars: loopPrompts().vars(name) }];
33
+ vars: loopPrompts().vars(name),
34
+ ...(mine.has(name) ? { fromFile: true } : {}) }];
30
35
  });
31
36
  }
32
37
  /** One record's declared variables, so a caller can offer or validate them before a run starts.
@@ -0,0 +1,74 @@
1
+ import type { LoopSourceInfo } from '../contracts.ts';
2
+ import type { LoopProtocolText, LoopPromptSource } from './loop-prompts-schema.ts';
3
+ /** A user file: a whole document in shape, any part of it in content. */
4
+ export interface LoopOverlaySource {
5
+ readonly version: number;
6
+ /** Global defaults this file moves; each field it omits keeps the shipped value. */
7
+ readonly defaults?: {
8
+ readonly score?: number;
9
+ readonly tries?: number;
10
+ };
11
+ /** Records this file adds or replaces, keyed by the name `/loop` takes. */
12
+ readonly protocols?: Readonly<Record<string, LoopProtocolText>>;
13
+ }
14
+ /** The records one process runs, and where they came from. */
15
+ export interface LoopSourceLoad {
16
+ /** The merged table the renderer reads: shipped records, then the user's on top. */
17
+ source: LoopPromptSource;
18
+ /** Which files were read, what the user's file changed, and any note for the operator. */
19
+ info: LoopSourceInfo;
20
+ }
21
+ /** Where the records come from for one process. */
22
+ export interface LoopSourceOptions {
23
+ /** Overlay to read instead of `<configDirectory>/loop.yaml`, from `--loop-file`/`DSHT_LOOP_FILE`. */
24
+ overlayFile?: string;
25
+ /** Configuration directory holding `loop.yaml` when no explicit file is given. */
26
+ configDirectory: string;
27
+ /** State directory holding the override stamp; omit to skip drift reporting. */
28
+ stateDirectory?: string;
29
+ /** Shipped file to read; defaults to the `loop.yaml` beside the package entry point. */
30
+ builtinFile?: string;
31
+ /** Table used when the shipped file cannot be read; defaults to the compiled-in one. */
32
+ fallback?: LoopPromptSource;
33
+ }
34
+ /** Path of the `loop.yaml` shipped beside this module.
35
+ *
36
+ * The path is relative to the module, so it resolves both in the source tree (`src/controller/`) and
37
+ * in the published build (`dist/controller/`), exactly as the version read in `cli/dsht.tsx` does.
38
+ * @returns Absolute path to the shipped record file.
39
+ */
40
+ export declare function shippedLoopFile(): string;
41
+ /** The overlay file a process reads, if it reads one at all.
42
+ *
43
+ * An explicit file wins over the configuration directory: it is how a script or a second checkout
44
+ * points at another set of records without moving the one the interactive client uses.
45
+ * @param options - Resolved options.
46
+ * @returns Absolute path of the overlay to try.
47
+ */
48
+ export declare function loopOverlayFile(options: LoopSourceOptions): string;
49
+ /** Layer one user file over the shipped records.
50
+ *
51
+ * A record is taken whole: replacing one does not merge its fields with the shipped copy, because a
52
+ * record is a single prompt contract and a half-new brief with a half-old round table is a protocol
53
+ * nobody wrote. Records the file does not name are untouched, which is what lets a shipped fix reach
54
+ * an install that customises something else.
55
+ * @param builtin - Shipped records.
56
+ * @param overlay - Parsed user file.
57
+ * @returns The merged table and the names the user file replaced or added.
58
+ */
59
+ export declare function mergeLoopSource(builtin: LoopPromptSource, overlay: LoopOverlaySource): {
60
+ source: LoopPromptSource;
61
+ overridden: string[];
62
+ added: string[];
63
+ };
64
+ /** Read the merged records for one process.
65
+ *
66
+ * The shipped file falls back to the compiled-in table with a warning, because a packaging mistake
67
+ * should not stop the client; a user file does not, because an invalid override the operator wrote is
68
+ * a mistake they can fix and a silent fallback would run the shipped records while they believe their
69
+ * own are in force. An absent overlay is ordinary: most installs have none.
70
+ * @param options - Resolution options.
71
+ * @returns The merged records, their sources and the notes to show the operator.
72
+ * @throws when the overlay exists but is not a valid loop file.
73
+ */
74
+ export declare function loadLoopSource(options: LoopSourceOptions): Promise<LoopSourceLoad>;