@danceiny/gotry 0.2.0-rc.27 → 0.2.0-rc.28
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/bin/gotry-bootstrap.js +47 -3
- package/dist/capabilities/anything.js +26 -0
- package/dist/capabilities/doctor.js +11 -4
- package/dist/capabilities/ground-transfer.js +168 -1
- package/dist/capabilities/provider-detail-sanitize.js +64 -0
- package/dist/capabilities/route-provider-conformance.js +341 -0
- package/dist/scripts/anything-path-measure-tests.js +133 -0
- package/dist/scripts/anything-path-measure.js +150 -0
- package/dist/scripts/anything-tests.js +136 -2
- package/dist/scripts/booking-copilot-dsh-warmer-proof.js +5 -2
- package/dist/scripts/bootstrap-tests.js +71 -1
- package/dist/scripts/city-scenario-tier-tests.js +234 -0
- package/dist/scripts/doctor-tests.js +37 -1
- package/dist/scripts/drill-multiuser-ledger-tests.js +1076 -0
- package/dist/scripts/drill-sdk-descendant-cleanup-tests.js +606 -0
- package/dist/scripts/drill-w2a-sensor-tests.js +788 -0
- package/dist/scripts/dsh-llm-tests.js +437 -0
- package/dist/scripts/g5-guard-tests.js +20 -1
- package/dist/scripts/g5-guard.js +5 -1
- package/dist/scripts/m3-cohort-tests.js +714 -0
- package/dist/scripts/m3-cohort.js +310 -0
- package/dist/scripts/nightly-evidence-tests.js +44 -2
- package/dist/scripts/nightly-evidence.js +21 -4
- package/dist/scripts/outcome-projection-tests.js +427 -0
- package/dist/scripts/persona-sim-tests.js +603 -0
- package/dist/scripts/persona-sim.js +1081 -0
- package/dist/scripts/replay-async.js +2 -0
- package/dist/scripts/replay.js +2 -0
- package/dist/scripts/route-provider-conformance-tests.js +391 -0
- package/dist/scripts/session-zone-crash.js +49 -0
- package/dist/scripts/session-zone-ledger-tests.js +432 -0
- package/dist/scripts/session-zone-observation-tests.js +492 -0
- package/dist/scripts/session-zone-observe.js +135 -0
- package/dist/scripts/session-zone-wiring-tests.js +566 -0
- package/dist/scripts/session-zones-tests.js +17 -1
- package/dist/scripts/spawn-bounded-abort-tests.js +3 -0
- package/dist/scripts/state-cli.js +16 -0
- package/dist/scripts/time-eval-tests.js +2 -0
- package/dist/scripts/unified-tests.js +37 -1
- package/dist/scripts/z3-race-tests.js +6 -1
- package/dist/src/city-scenario-tier.js +378 -0
- package/dist/src/dsh-llm.js +84 -14
- package/dist/src/index.js +160 -1
- package/dist/src/loop.js +16 -5
- package/dist/src/m3-cohort.js +1577 -0
- package/dist/src/outcome-projection.js +746 -0
- package/dist/src/session-zone-ledger.js +312 -0
- package/dist/src/session-zone-observation.js +354 -0
- package/dist/src/session-zone-wiring.js +658 -0
- package/dist/src/session-zones.js +7 -4
- package/dist/src/state-ledger.js +293 -40
- package/dist/src/unified.js +69 -5
- package/extension/manifest.json +2 -2
- package/package.json +1 -1
- package/ts/capabilities/anything.ts +27 -0
- package/ts/scripts/state-cli.ts +18 -0
- package/ts/src/contracts.ts +4 -0
- package/ts/src/dsh-llm.ts +111 -9
- package/ts/src/index.ts +175 -2
- package/ts/src/loop.ts +15 -5
- package/ts/src/state-ledger.ts +294 -40
- package/ts/src/unified.ts +91 -18
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Explicit opt-in M3 seed-cohort capture CLI (issue #22).
|
|
4
|
+
*
|
|
5
|
+
* Produces the records `scripts/product-metrics.ts` consumes. The scorer is the only
|
|
6
|
+
* component that divides anything; this CLI only records funnel facts and then hands the
|
|
7
|
+
* written evidence root straight back to the scorer for verification.
|
|
8
|
+
*
|
|
9
|
+
* The production CLI intentionally has no `--at` flag: timestamps come from the local
|
|
10
|
+
* clock. Tests inject clocks through `../src/m3-cohort.ts`.
|
|
11
|
+
*
|
|
12
|
+
* HMAC key custody: `GOTRY_M3_COHORT_HMAC_KEY` (>= 32 chars) lives outside the repository
|
|
13
|
+
* and is never printed by this CLI. Losing it makes the store unreadable by design, and
|
|
14
|
+
* `verify` needs it too — the export attestation is keyed, not a public digest.
|
|
15
|
+
*
|
|
16
|
+
* Argv exposure (accepted, documented): `--participant`, `--plan` and `--consent` carry raw
|
|
17
|
+
* labels on the command line, so they are visible to `ps` and land in shell history. Only
|
|
18
|
+
* their HMAC refs are ever persisted, but the invocation itself is not secret. For a real
|
|
19
|
+
* cohort, drive this CLI from a script whose history is not shared, or export the labels
|
|
20
|
+
* from a file the shell does not record; never pass anything you would not put in `ps`.
|
|
21
|
+
*
|
|
22
|
+
* npx tsx scripts/m3-cohort.ts init --state-root "$ROOT" --consent "$C" \
|
|
23
|
+
* --cohort m3-seed --evidence-kind real_seed_cohort \
|
|
24
|
+
* --window-start 2026-10-01T00:00:00.000Z --window-end 2026-12-31T23:59:59.000Z \
|
|
25
|
+
* --timezone Asia/Shanghai --attribution gotry_primary
|
|
26
|
+
* npx tsx scripts/m3-cohort.ts enroll ... --participant <handle> --invited --participant-consent
|
|
27
|
+
* npx tsx scripts/m3-cohort.ts deliver ... --participant <handle> --plan <handle> --attribution gotry_primary
|
|
28
|
+
* npx tsx scripts/m3-cohort.ts finalize ... --participant <handle> --plan <handle>
|
|
29
|
+
* npx tsx scripts/m3-cohort.ts nps ... --participant <handle> --plan <handle> --score 9
|
|
30
|
+
* npx tsx scripts/m3-cohort.ts poi-lock ... --participant <handle> --plan <handle> --locked-claims 12 --invalid-claims 0
|
|
31
|
+
* npx tsx scripts/m3-cohort.ts export ... --evidence-root <dir>
|
|
32
|
+
* npx tsx scripts/m3-cohort.ts verify --evidence-root <dir> # needs the HMAC key
|
|
33
|
+
* npx tsx scripts/m3-cohort.ts lock-status --state-root "$ROOT" # read-only
|
|
34
|
+
* npx tsx scripts/m3-cohort.ts unlock --state-root "$ROOT" # dead writer only
|
|
35
|
+
*/
|
|
36
|
+
import { resolve } from 'node:path';
|
|
37
|
+
import { pathToFileURL } from 'node:url';
|
|
38
|
+
import { enrollParticipant, exportM3Cohort, initM3Cohort, inspectStoreLock, M3CohortError, M3_SIMULATION_PROVENANCE_SCHEMA, recordNps, recordPlanDelivered, recordPlanFinalized, recordPoiLock, unlockStore, verifyExportedEvidence, } from '../src/m3-cohort.js';
|
|
39
|
+
import { parseCohortRecord, parseManifest, scoreProductMetrics } from './product-metrics.js';
|
|
40
|
+
const ERROR_CODES = [
|
|
41
|
+
'bad_args',
|
|
42
|
+
'cohort_already_initialized',
|
|
43
|
+
'cohort_consent_mismatch',
|
|
44
|
+
'cohort_key_mismatch',
|
|
45
|
+
'cohort_not_initialized',
|
|
46
|
+
'evidence_kind_mismatch',
|
|
47
|
+
'evidence_root_conflict',
|
|
48
|
+
'export_validation_failed',
|
|
49
|
+
'internal_error',
|
|
50
|
+
'invalid_hmac_key',
|
|
51
|
+
'invalid_input',
|
|
52
|
+
'invalid_store',
|
|
53
|
+
'invalid_transition',
|
|
54
|
+
'lock_busy',
|
|
55
|
+
'missing_consent',
|
|
56
|
+
'missing_hmac_key',
|
|
57
|
+
'missing_state_root',
|
|
58
|
+
'output_exists',
|
|
59
|
+
'simulation_forbidden_in_real_cohort',
|
|
60
|
+
'simulation_label_required',
|
|
61
|
+
'unsafe_state_root',
|
|
62
|
+
];
|
|
63
|
+
function parseFlags(args) {
|
|
64
|
+
const [command, ...rest] = args;
|
|
65
|
+
if (!command)
|
|
66
|
+
throw new M3CohortError('bad_args');
|
|
67
|
+
const flags = {};
|
|
68
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
69
|
+
const token = rest[index];
|
|
70
|
+
if (!token.startsWith('--'))
|
|
71
|
+
throw new M3CohortError('bad_args');
|
|
72
|
+
const key = token.slice(2);
|
|
73
|
+
if (!key)
|
|
74
|
+
throw new M3CohortError('bad_args');
|
|
75
|
+
const next = rest[index + 1];
|
|
76
|
+
const value = next === undefined || next.startsWith('--') ? true : (index += 1, next);
|
|
77
|
+
if (key in flags) {
|
|
78
|
+
const current = flags[key];
|
|
79
|
+
flags[key] = Array.isArray(current) ? [...current, value] : [current, value];
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
flags[key] = value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { command, flags };
|
|
86
|
+
}
|
|
87
|
+
function one(flags, key, required = true) {
|
|
88
|
+
const value = flags[key];
|
|
89
|
+
if (Array.isArray(value) || value === true)
|
|
90
|
+
throw new M3CohortError('bad_args');
|
|
91
|
+
if (value === undefined && required)
|
|
92
|
+
throw new M3CohortError(key === 'state-root' ? 'missing_state_root' : 'invalid_input');
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
function many(flags, key) {
|
|
96
|
+
const value = flags[key];
|
|
97
|
+
if (value === undefined)
|
|
98
|
+
return [];
|
|
99
|
+
if (value === true)
|
|
100
|
+
throw new M3CohortError('bad_args');
|
|
101
|
+
const list = Array.isArray(value) ? value : [value];
|
|
102
|
+
return list.map(item => {
|
|
103
|
+
if (item === true)
|
|
104
|
+
throw new M3CohortError('bad_args');
|
|
105
|
+
return item;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function boolFlag(flags, key) {
|
|
109
|
+
const value = flags[key];
|
|
110
|
+
if (value === undefined)
|
|
111
|
+
return false;
|
|
112
|
+
if (value !== true)
|
|
113
|
+
throw new M3CohortError('bad_args');
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
function exclusiveBool(flags, trueKey, falseKey) {
|
|
117
|
+
const positive = boolFlag(flags, trueKey);
|
|
118
|
+
const negative = boolFlag(flags, falseKey);
|
|
119
|
+
if (positive === negative)
|
|
120
|
+
throw new M3CohortError('invalid_input');
|
|
121
|
+
return positive;
|
|
122
|
+
}
|
|
123
|
+
function integerFlag(flags, key) {
|
|
124
|
+
const raw = one(flags, key);
|
|
125
|
+
if (!/^\d{1,9}$/.test(raw))
|
|
126
|
+
throw new M3CohortError('invalid_input');
|
|
127
|
+
return Number(raw);
|
|
128
|
+
}
|
|
129
|
+
function common(flags) {
|
|
130
|
+
const stateRoot = one(flags, 'state-root');
|
|
131
|
+
const consent = one(flags, 'consent', false);
|
|
132
|
+
if (!consent)
|
|
133
|
+
throw new M3CohortError('missing_consent');
|
|
134
|
+
return { stateRoot: stateRoot, consent, hmacKey: process.env['GOTRY_M3_COHORT_HMAC_KEY'] };
|
|
135
|
+
}
|
|
136
|
+
function ensureNoUnknownFlags(flags, allowed) {
|
|
137
|
+
const allowedSet = new Set(allowed);
|
|
138
|
+
for (const key of Object.keys(flags)) {
|
|
139
|
+
if (!allowedSet.has(key))
|
|
140
|
+
throw new M3CohortError('bad_args');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function attributionFlag(flags) {
|
|
144
|
+
const value = one(flags, 'attribution');
|
|
145
|
+
if (value !== 'gotry_primary' && value !== 'gotry_assisted')
|
|
146
|
+
throw new M3CohortError('invalid_input');
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
function attributionList(flags) {
|
|
150
|
+
const values = many(flags, 'attribution');
|
|
151
|
+
if (values.length === 0)
|
|
152
|
+
throw new M3CohortError('invalid_input');
|
|
153
|
+
return values.map(value => {
|
|
154
|
+
if (value !== 'gotry_primary' && value !== 'gotry_assisted')
|
|
155
|
+
throw new M3CohortError('invalid_input');
|
|
156
|
+
return value;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function simulationFlags(flags) {
|
|
160
|
+
const simulated = boolFlag(flags, 'simulated');
|
|
161
|
+
const personaId = one(flags, 'persona-id', false);
|
|
162
|
+
const productModel = one(flags, 'product-model', false);
|
|
163
|
+
const personaModel = one(flags, 'persona-model', false);
|
|
164
|
+
const promptDigest = one(flags, 'prompt-digest', false);
|
|
165
|
+
const provided = [personaId, productModel, personaModel, promptDigest];
|
|
166
|
+
if (!simulated) {
|
|
167
|
+
if (provided.some(value => value !== undefined))
|
|
168
|
+
throw new M3CohortError('invalid_input');
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
if (provided.some(value => value === undefined))
|
|
172
|
+
throw new M3CohortError('invalid_input');
|
|
173
|
+
return {
|
|
174
|
+
schema_version: M3_SIMULATION_PROVENANCE_SCHEMA,
|
|
175
|
+
kind: 'llm_persona',
|
|
176
|
+
persona_id: personaId,
|
|
177
|
+
product_model: productModel,
|
|
178
|
+
persona_model: personaModel,
|
|
179
|
+
prompt_digest: promptDigest,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function output(value) {
|
|
183
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
184
|
+
}
|
|
185
|
+
/** Consumer-side verification: the exact parsers and scorer the M3 gate uses. */
|
|
186
|
+
export function scorerValidator(payload) {
|
|
187
|
+
const manifest = parseManifest(JSON.parse(JSON.stringify(payload.manifest)));
|
|
188
|
+
const cohort = payload.cohort.map((record, index) => parseCohortRecord(JSON.parse(JSON.stringify(record)), index));
|
|
189
|
+
const summary = scoreProductMetrics(manifest, cohort, []);
|
|
190
|
+
if (manifest.evidence_kind === 'synthetic_fixture' && summary.business_pass) {
|
|
191
|
+
throw new M3CohortError('export_validation_failed');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function run(command, flags) {
|
|
195
|
+
switch (command) {
|
|
196
|
+
case 'init': {
|
|
197
|
+
ensureNoUnknownFlags(flags, ['state-root', 'consent', 'cohort', 'evidence-kind', 'window-start', 'window-end', 'timezone', 'attribution']);
|
|
198
|
+
const evidenceKind = one(flags, 'evidence-kind');
|
|
199
|
+
if (evidenceKind !== 'real_seed_cohort' && evidenceKind !== 'synthetic_fixture')
|
|
200
|
+
throw new M3CohortError('invalid_input');
|
|
201
|
+
output(initM3Cohort({
|
|
202
|
+
...common(flags),
|
|
203
|
+
cohort: one(flags, 'cohort'),
|
|
204
|
+
evidenceKind,
|
|
205
|
+
windowStartAt: one(flags, 'window-start'),
|
|
206
|
+
windowEndAt: one(flags, 'window-end'),
|
|
207
|
+
timezone: one(flags, 'timezone'),
|
|
208
|
+
allowedAttribution: attributionList(flags),
|
|
209
|
+
}));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
case 'enroll': {
|
|
213
|
+
ensureNoUnknownFlags(flags, [
|
|
214
|
+
'state-root', 'consent', 'participant', 'invited', 'not-invited', 'participant-consent',
|
|
215
|
+
'participant-consent-withdrawn', 'test-or-staff', 'simulated', 'persona-id', 'product-model',
|
|
216
|
+
'persona-model', 'prompt-digest',
|
|
217
|
+
]);
|
|
218
|
+
output(enrollParticipant({
|
|
219
|
+
...common(flags),
|
|
220
|
+
participant: one(flags, 'participant'),
|
|
221
|
+
invited: exclusiveBool(flags, 'invited', 'not-invited'),
|
|
222
|
+
participantConsent: exclusiveBool(flags, 'participant-consent', 'participant-consent-withdrawn'),
|
|
223
|
+
testOrStaff: boolFlag(flags, 'test-or-staff'),
|
|
224
|
+
simulation: simulationFlags(flags),
|
|
225
|
+
}));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
case 'deliver': {
|
|
229
|
+
ensureNoUnknownFlags(flags, ['state-root', 'consent', 'participant', 'plan', 'attribution']);
|
|
230
|
+
output(recordPlanDelivered({
|
|
231
|
+
...common(flags),
|
|
232
|
+
participant: one(flags, 'participant'),
|
|
233
|
+
plan: one(flags, 'plan'),
|
|
234
|
+
attribution: attributionFlag(flags),
|
|
235
|
+
}));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
case 'finalize': {
|
|
239
|
+
ensureNoUnknownFlags(flags, ['state-root', 'consent', 'participant', 'plan']);
|
|
240
|
+
output(recordPlanFinalized({ ...common(flags), participant: one(flags, 'participant'), plan: one(flags, 'plan') }));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
case 'nps': {
|
|
244
|
+
ensureNoUnknownFlags(flags, ['state-root', 'consent', 'participant', 'plan', 'score']);
|
|
245
|
+
output(recordNps({
|
|
246
|
+
...common(flags),
|
|
247
|
+
participant: one(flags, 'participant'),
|
|
248
|
+
plan: one(flags, 'plan'),
|
|
249
|
+
score: integerFlag(flags, 'score'),
|
|
250
|
+
}));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
case 'poi-lock': {
|
|
254
|
+
ensureNoUnknownFlags(flags, ['state-root', 'consent', 'participant', 'plan', 'locked-claims', 'invalid-claims']);
|
|
255
|
+
output(recordPoiLock({
|
|
256
|
+
...common(flags),
|
|
257
|
+
participant: one(flags, 'participant'),
|
|
258
|
+
plan: one(flags, 'plan'),
|
|
259
|
+
lockedClaims: integerFlag(flags, 'locked-claims'),
|
|
260
|
+
invalidClaims: integerFlag(flags, 'invalid-claims'),
|
|
261
|
+
}));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
case 'export': {
|
|
265
|
+
ensureNoUnknownFlags(flags, ['state-root', 'consent', 'evidence-root']);
|
|
266
|
+
const evidenceRoot = one(flags, 'evidence-root');
|
|
267
|
+
const shared = common(flags);
|
|
268
|
+
const { payload, result } = exportM3Cohort({ ...shared, evidenceRoot, validate: scorerValidator });
|
|
269
|
+
// Verify the bytes that actually landed: re-read the evidence root through the real
|
|
270
|
+
// scorer parsers plus the keyed attestation check.
|
|
271
|
+
const manifest = parseManifest(JSON.parse(JSON.stringify(payload.manifest)));
|
|
272
|
+
const cohort = payload.cohort.map((record, index) => parseCohortRecord(JSON.parse(JSON.stringify(record)), index));
|
|
273
|
+
const summary = scoreProductMetrics(manifest, cohort, []);
|
|
274
|
+
const verification = verifyExportedEvidence(evidenceRoot, shared.hmacKey);
|
|
275
|
+
output({ ...result, verification, scorer_summary: summary });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
case 'verify': {
|
|
279
|
+
ensureNoUnknownFlags(flags, ['evidence-root']);
|
|
280
|
+
output(verifyExportedEvidence(one(flags, 'evidence-root'), process.env['GOTRY_M3_COHORT_HMAC_KEY']));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
case 'lock-status': {
|
|
284
|
+
ensureNoUnknownFlags(flags, ['state-root']);
|
|
285
|
+
output(inspectStoreLock(one(flags, 'state-root')));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
case 'unlock': {
|
|
289
|
+
ensureNoUnknownFlags(flags, ['state-root']);
|
|
290
|
+
output(unlockStore(one(flags, 'state-root')));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
default:
|
|
294
|
+
throw new M3CohortError('bad_args');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function main() {
|
|
298
|
+
try {
|
|
299
|
+
const { command, flags } = parseFlags(process.argv.slice(2));
|
|
300
|
+
run(command, flags);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
const code = error instanceof M3CohortError && ERROR_CODES.includes(error.code) ? error.code : 'internal_error';
|
|
304
|
+
process.stderr.write(`m3_cohort_error:${code}\n`);
|
|
305
|
+
process.exitCode = 2;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : '';
|
|
309
|
+
if (import.meta.url === invokedPath)
|
|
310
|
+
main();
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* - 封存价表仅可审计换算(peak 保守上界),未知模型 fail-closed,usage 缺失 fail-closed;
|
|
5
5
|
* - run_key 确定性 + 记录必须过消费方 parseNightlyRun(生产器写的就是评分器读的);
|
|
6
6
|
* - 无凭证 = waiting_external_evidence,不写任何文件(issue #22 停机纪律);
|
|
7
|
-
* - --dry-run mock
|
|
7
|
+
* - --dry-run mock 全链演练可跑通且绝不落盘;
|
|
8
|
+
* - --dry-run 默认离线(issue #617):PATH 上的计数 fake hbcli 零次被调起(第三轮「我订了酒店:…」会触发 PoI 探针),
|
|
9
|
+
* operator 显式设置的 GOTRY_HBCLI_LIVE 不被覆盖(同时是 fake 可达的 positive control),演练后环境原样还原。
|
|
8
10
|
* 真跑(花钱)不在 CI:heartbeat/founder 手动 `npx tsx scripts/nightly-evidence.ts`。
|
|
9
11
|
*
|
|
10
12
|
* 价表 schema 演进(issue #49):v1 (legacy DeepSeek only) / v2 (provider-aware;DeepSeek+MiniMax);
|
|
@@ -13,7 +15,7 @@
|
|
|
13
15
|
* usage 缺失 fail-closed、run_key 确定性 + 消费方 parseNightlyRun、无凭证 waiting、dry-run 零落盘。
|
|
14
16
|
*/
|
|
15
17
|
import assert from 'node:assert/strict';
|
|
16
|
-
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
17
19
|
import { join } from 'node:path';
|
|
18
20
|
import { tmpdir } from 'node:os';
|
|
19
21
|
import { buildNightlyRunRecord, loadPriceTable, loadPromptSet, priceRunCost, runNightlyEvidence, } from './nightly-evidence.js';
|
|
@@ -158,4 +160,44 @@ finally {
|
|
|
158
160
|
rmSync(dryParent, { recursive: true, force: true });
|
|
159
161
|
}
|
|
160
162
|
ok('dry-run mock 全链演练 + real_llm=false + 零落盘');
|
|
163
|
+
// ---- 7b. issue #617:dry-run 是离线「mock 全链演练」,不得经 runTurn→probePoi→anythingSearch 连真实 hbcli 后端 ----
|
|
164
|
+
// 受控 PATH 上放计数 fake(只用 shell 内建);HOME 指向空目录,已知安装位回退碰不到开发机真实 hbcli。
|
|
165
|
+
const hbcliProbeRoot = mkdtempSync(join(tmpdir(), 'nightly-hbcli-'));
|
|
166
|
+
const hbcliProbeBin = join(hbcliProbeRoot, 'bin');
|
|
167
|
+
const hbcliCallsLog = join(hbcliProbeRoot, 'calls.log');
|
|
168
|
+
mkdirSync(hbcliProbeBin);
|
|
169
|
+
mkdirSync(join(hbcliProbeRoot, 'home'));
|
|
170
|
+
writeFileSync(join(hbcliProbeBin, 'hbcli'), `#!/bin/sh\nprintf 'call\\n' >> '${hbcliCallsLog}'\nprintf '%s' '{"candidates":[]}'\n`, { mode: 0o755 });
|
|
171
|
+
const hbcliCalls = () => existsSync(hbcliCallsLog) ? readFileSync(hbcliCallsLog, 'utf8').split('\n').filter(Boolean).length : 0;
|
|
172
|
+
const savedEnv = { PATH: process.env['PATH'], HOME: process.env['HOME'], LIVE: process.env['GOTRY_HBCLI_LIVE'] };
|
|
173
|
+
const restoreEnv = (key, value) => {
|
|
174
|
+
if (value === undefined)
|
|
175
|
+
delete process.env[key];
|
|
176
|
+
else
|
|
177
|
+
process.env[key] = value;
|
|
178
|
+
};
|
|
179
|
+
try {
|
|
180
|
+
process.env['PATH'] = `${hbcliProbeBin}:${savedEnv.PATH ?? ''}`;
|
|
181
|
+
process.env['HOME'] = join(hbcliProbeRoot, 'home');
|
|
182
|
+
const dryRoot2 = join(hbcliProbeRoot, 'evidence');
|
|
183
|
+
// 缺省(operator 未设):演练期间 GOTRY_HBCLI_LIVE=0,fake 零次被调起,演练后环境原样还原(无泄漏)
|
|
184
|
+
delete process.env['GOTRY_HBCLI_LIVE'];
|
|
185
|
+
const offlineDry = await runNightlyEvidence({ evidenceRoot: dryRoot2, dryRun: true, clock: () => new Date(executeAt) });
|
|
186
|
+
assert.equal(offlineDry.state, 'dry_run');
|
|
187
|
+
assert.equal(hbcliCalls(), 0, 'dry-run 缺省离线:PATH 上的 fake hbcli 必须被调起 0 次');
|
|
188
|
+
assert.equal(process.env['GOTRY_HBCLI_LIVE'], undefined, 'dry-run 结束后必须还原 GOTRY_HBCLI_LIVE(不得泄漏进后续进程内套件)');
|
|
189
|
+
// operator 显式设置的值不被覆盖——同时是 positive control:证明上面的「零次」不是因为 fake 不可达
|
|
190
|
+
process.env['GOTRY_HBCLI_LIVE'] = '1';
|
|
191
|
+
const explicitDry = await runNightlyEvidence({ evidenceRoot: dryRoot2, dryRun: true, clock: () => new Date(executeAt) });
|
|
192
|
+
assert.equal(explicitDry.state, 'dry_run');
|
|
193
|
+
assert.ok(hbcliCalls() >= 1, `显式 GOTRY_HBCLI_LIVE=1 不得被 dry-run 覆盖,PoI 探针应调起 fake(实际 ${hbcliCalls()} 次)`);
|
|
194
|
+
assert.equal(process.env['GOTRY_HBCLI_LIVE'], '1', '显式值演练后原样保留');
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
restoreEnv('PATH', savedEnv.PATH);
|
|
198
|
+
restoreEnv('HOME', savedEnv.HOME);
|
|
199
|
+
restoreEnv('GOTRY_HBCLI_LIVE', savedEnv.LIVE);
|
|
200
|
+
rmSync(hbcliProbeRoot, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
ok('dry-run 缺省 GOTRY_HBCLI_LIVE=0:fake hbcli 零次调起 + 环境还原;显式值不覆盖(positive control)');
|
|
161
203
|
console.log(`nightly-evidence tests: ${passed} 组断言全绿(offline,真跑不在 CI)`);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 运行(在 ts/ 下):
|
|
8
8
|
* npx tsx scripts/nightly-evidence.ts # 真跑:需 LLM_API_KEY,追加证据记录
|
|
9
|
-
* npx tsx scripts/nightly-evidence.ts --dry-run # mock 全链演练,绝不写证据
|
|
9
|
+
* npx tsx scripts/nightly-evidence.ts --dry-run # mock 全链演练,绝不写证据(缺省 GOTRY_HBCLI_LIVE=0:不连真实 hbcli;显式设置则尊重)
|
|
10
10
|
* npx tsx scripts/nightly-evidence.ts --format json # 机器可读状态(heartbeat 消费)
|
|
11
11
|
* npx tsx scripts/nightly-evidence.ts --no-env-file # 不读仓根 .env(等待态测试用)
|
|
12
12
|
*
|
|
@@ -190,6 +190,17 @@ function arg(name) {
|
|
|
190
190
|
const index = process.argv.indexOf(name);
|
|
191
191
|
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
192
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* issue #617:`--dry-run` 是离线「mock 全链演练」,但 runTurn 的 PoI 探针会 spawn 本机真实 hbcli
|
|
195
|
+
* (带凭据即连真实后端、耗配额)。演练期间把离线开关缺省置 0——operator 显式设过 GOTRY_HBCLI_LIVE
|
|
196
|
+
* (含空串)则一律不覆盖;返回还原函数(本函数在进程内被测试与 persona harness 复用,不得泄漏环境)。
|
|
197
|
+
*/
|
|
198
|
+
function defaultHbcliLiveOff() {
|
|
199
|
+
if (process.env['GOTRY_HBCLI_LIVE'] !== undefined)
|
|
200
|
+
return () => { };
|
|
201
|
+
process.env['GOTRY_HBCLI_LIVE'] = '0';
|
|
202
|
+
return () => { delete process.env['GOTRY_HBCLI_LIVE']; };
|
|
203
|
+
}
|
|
193
204
|
export async function runNightlyEvidence(options) {
|
|
194
205
|
const clock = options.clock ?? (() => new Date());
|
|
195
206
|
const llmKey = process.env['LLM_API_KEY'] ?? process.env['DEEPSEEK_API_KEY'];
|
|
@@ -203,9 +214,15 @@ export async function runNightlyEvidence(options) {
|
|
|
203
214
|
const port = options.dryRun ? createMockLlm(packPath) : createOpenAICompatLlm(packPath);
|
|
204
215
|
const state = newState();
|
|
205
216
|
const history = [];
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
217
|
+
const restoreHbcliLive = options.dryRun ? defaultHbcliLiveOff() : () => { };
|
|
218
|
+
try {
|
|
219
|
+
for (const turn of promptSet.turns) {
|
|
220
|
+
const { reply } = await runTurn(state, turn, port, [...history], solvePort);
|
|
221
|
+
history.push({ role: 'user', text: turn }, { role: 'assistant', text: reply });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
restoreHbcliLive();
|
|
209
226
|
}
|
|
210
227
|
const promptSetSha = sha256Of(promptSet);
|
|
211
228
|
// history 是 user/assistant 交错序列;assistant 回复落在每个 user 之后(index*2+1)
|