@zfdx123/dsh-hooks-ordering 1.0.4 → 1.0.6
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/client.js +352 -54
- package/lib/index.d.ts +38 -8
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +59 -50
- package/lib/index.js.map +1 -1
- package/package.json +6 -6
- package/src/dsh.ts +48 -15
- package/src/index.ts +2 -1
- package/src/settings.ts +63 -64
package/client.js
CHANGED
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
// - the file is a classic script, so it has no import/export: React arrives
|
|
15
15
|
// through the synchronous `require` handed to the factory.
|
|
16
16
|
//
|
|
17
|
-
// Writes go through
|
|
18
|
-
//
|
|
19
|
-
//
|
|
17
|
+
// Writes go through two path-addressed ops rather than a scalar setter: the
|
|
18
|
+
// fields are string arrays, and `{op:'set'|'unset', path}` says exactly what is
|
|
19
|
+
// meant for both save and reset. They are sent to the settings Remote namespace
|
|
20
|
+
// (`ctx.remote.settings.mutate`), which resolves them against the stored section
|
|
21
|
+
// rather than against whatever this page last read.
|
|
20
22
|
//
|
|
21
23
|
// The namespace is `applies: 'restart'`, so the page says so instead of
|
|
22
24
|
// pretending a save took effect immediately.
|
|
@@ -102,7 +104,7 @@
|
|
|
102
104
|
fieldHooks: 'waterfall 钩子(每行一个)',
|
|
103
105
|
fieldSerialHooks: 'serial 钩子(每行一个)',
|
|
104
106
|
fieldLog: '约束 DAG 日志文件(留空即不记录)',
|
|
105
|
-
note: '改动在重启 dsh
|
|
107
|
+
note: '改动在重启 dsh 后生效(hooks/serialHooks 的变更需要重新挂载钩子)。清空某字段即回到 schema 默认值。',
|
|
106
108
|
saveFailed: '保存失败:{message}',
|
|
107
109
|
saving: '保存中…',
|
|
108
110
|
save: '保存',
|
|
@@ -119,7 +121,7 @@
|
|
|
119
121
|
fieldHooks: 'Waterfall hooks (one per line)',
|
|
120
122
|
fieldSerialHooks: 'Serial hooks (one per line)',
|
|
121
123
|
fieldLog: 'Constraint DAG log file (leave empty to log nothing)',
|
|
122
|
-
note: 'Changes take effect after dsh restarts (
|
|
124
|
+
note: 'Changes take effect after dsh restarts (editing hooks/serialHooks re-mounts the hook brackets). Clearing a field returns it to the schema default.',
|
|
123
125
|
saveFailed: 'Save failed: {message}',
|
|
124
126
|
saving: 'Saving…',
|
|
125
127
|
save: 'Save',
|
|
@@ -344,17 +346,285 @@
|
|
|
344
346
|
return style
|
|
345
347
|
}
|
|
346
348
|
|
|
349
|
+
/**
|
|
350
|
+
* The name this plugin's settings entry actually has, resolved at runtime
|
|
351
|
+
* from the descriptors the host returns.
|
|
352
|
+
*
|
|
353
|
+
* It CANNOT be a constant. The settings namespace is the **loader entry
|
|
354
|
+
* id**, which belongs to whoever wrote the row that mounted this plugin,
|
|
355
|
+
* not to the plugin: the aggregator's `cordis.patch.yml` uses
|
|
356
|
+
* `dsh-plugin-hooks-ordering`, while this package's own patch uses
|
|
357
|
+
* `hooks-ordering`. Hardcoding the latter against the former produced
|
|
358
|
+
*
|
|
359
|
+
* no settings entry is named "hooks-ordering" (describe() returned: …, dsh-plugin-hooks-ordering, …)
|
|
360
|
+
*
|
|
361
|
+
* and the write path was refused outright:
|
|
362
|
+
*
|
|
363
|
+
* settings/rejected: No configurable plugin entry "hooks-ordering"
|
|
364
|
+
*
|
|
365
|
+
* Matching on the descriptor's own `ns` removes the guesswork: whatever id
|
|
366
|
+
* the mounting row has, this is the entry it produced.
|
|
367
|
+
*/
|
|
368
|
+
var resolvedNs = null
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Normalise a Remote answer to `{ ok, value }` / `{ ok: false, reason }`.
|
|
372
|
+
*
|
|
373
|
+
* The 0.1.7 Remote protocol answers every call with a `RemoteResult`
|
|
374
|
+
* envelope and the client proxy hands that envelope through as-is, so a
|
|
375
|
+
* caller that reads fields off the answer directly sees `undefined` while
|
|
376
|
+
* the wire traffic is healthy. A plain value is accepted too, so this
|
|
377
|
+
* keeps working if a future runtime unwraps for us.
|
|
378
|
+
*
|
|
379
|
+
* @param answer - whatever the Remote call resolved to.
|
|
380
|
+
* @returns the unwrapped outcome.
|
|
381
|
+
*/
|
|
382
|
+
function unwrapRemote(answer) {
|
|
383
|
+
if (answer !== null && typeof answer === 'object' && typeof answer.ok === 'boolean') {
|
|
384
|
+
if (answer.ok === false) {
|
|
385
|
+
var failure = answer.error
|
|
386
|
+
var reason =
|
|
387
|
+
failure && typeof failure === 'object'
|
|
388
|
+
? String(failure.code || 'remote error') + ': ' + String(failure.message || '')
|
|
389
|
+
: String(failure || 'remote error')
|
|
390
|
+
return { ok: false, reason: reason }
|
|
391
|
+
}
|
|
392
|
+
return { ok: true, value: answer.value }
|
|
393
|
+
}
|
|
394
|
+
return { ok: true, value: answer }
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* @param list - descriptor list from `describe()`.
|
|
399
|
+
* @returns the descriptor for this plugin, or undefined.
|
|
400
|
+
*/
|
|
401
|
+
function pickEntry(list) {
|
|
402
|
+
// Fast path: the row used this package's own documented id.
|
|
403
|
+
var direct = list.find(function (entry) {
|
|
404
|
+
return entry && entry.ns === NS
|
|
405
|
+
})
|
|
406
|
+
if (direct !== undefined) return direct
|
|
407
|
+
// Otherwise take the entry that was mounting this plugin created.
|
|
408
|
+
// `dsh-plugin-hooks-ordering` is the shipped aggregator row; the suffix
|
|
409
|
+
// test also covers a user's own row name.
|
|
410
|
+
var suffixed = list.filter(function (entry) {
|
|
411
|
+
return entry && typeof entry.ns === 'string' && entry.ns.slice(-NS.length) === NS
|
|
412
|
+
})
|
|
413
|
+
return suffixed.length === 1 ? suffixed[0] : undefined
|
|
414
|
+
}
|
|
415
|
+
|
|
347
416
|
// ── the settings page ──────────────────────────────────────────────────
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Interpret the setting page's view of one entry.
|
|
420
|
+
*
|
|
421
|
+
* Three inputs can describe it: an already-assembled view (`{status,
|
|
422
|
+
* value, ...}`), a raw settings descriptor (`{ns, value, revision}`), or
|
|
423
|
+
* nothing at all.
|
|
424
|
+
*
|
|
425
|
+
* @param raw - one of the shapes above, or null/undefined.
|
|
426
|
+
* @returns a view this page can render, or `null` when there is no entry.
|
|
427
|
+
*/
|
|
428
|
+
function normaliseSnapshot(raw) {
|
|
429
|
+
if (raw === null || raw === undefined) return null
|
|
430
|
+
var writable = raw.writable !== false
|
|
431
|
+
if (raw.status === 'unavailable' || raw.available === false) {
|
|
432
|
+
return {
|
|
433
|
+
status: 'unavailable',
|
|
434
|
+
available: false,
|
|
435
|
+
writable: false,
|
|
436
|
+
revision: undefined,
|
|
437
|
+
value: null,
|
|
438
|
+
base: {},
|
|
439
|
+
reason: raw.reason,
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
status: 'ready',
|
|
444
|
+
available: true,
|
|
445
|
+
writable: writable,
|
|
446
|
+
revision: raw.revision,
|
|
447
|
+
value: raw.value || null,
|
|
448
|
+
base: raw.base || {},
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Adapt the 0.1.7 `ctx.remote.settings` namespace to the snapshot surface
|
|
454
|
+
* this page renders from.
|
|
455
|
+
*
|
|
456
|
+
* 0.1.6 exposed a `settingsScope` service that answered snapshots
|
|
457
|
+
* synchronously. 0.1.7 removed it: a page now talks to the settings
|
|
458
|
+
* Remote, whose `describe()`/`mutate()` are async and keyed by the
|
|
459
|
+
* profile entry id. This adapter resolves the descriptor once, re-resolves
|
|
460
|
+
* it on the host's `settings/document-updated` notification.
|
|
461
|
+
*
|
|
462
|
+
* @param ctx - the plugin context (provides `remote`).
|
|
463
|
+
*/
|
|
464
|
+
function createSettingsSource(ctx) {
|
|
465
|
+
var remote = ctx && ctx.remote && ctx.remote.settings
|
|
466
|
+
/**
|
|
467
|
+
* The name this plugin's settings entry actually has is resolved at
|
|
468
|
+
* runtime by {@link pickEntry} into the module-level `resolvedNs`; see
|
|
469
|
+
* the note there for why it cannot be a constant.
|
|
470
|
+
*/
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Why the form has no data. Kept as a concrete, user-visible string:
|
|
474
|
+
* "not available" on its own made a wiring fault indistinguishable from
|
|
475
|
+
* a not-yet-mounted host half, and that cost a lot of blind debugging.
|
|
476
|
+
*/
|
|
477
|
+
function unavailableBecause(reason) {
|
|
478
|
+
return {
|
|
479
|
+
status: 'unavailable',
|
|
480
|
+
available: false,
|
|
481
|
+
writable: false,
|
|
482
|
+
value: null,
|
|
483
|
+
revision: undefined,
|
|
484
|
+
reason: reason,
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* @returns a promise of the current descriptor view, or of an
|
|
490
|
+
* `unavailable` view carrying the concrete reason.
|
|
491
|
+
*/
|
|
492
|
+
function read() {
|
|
493
|
+
if (!ctx || !ctx.remote) {
|
|
494
|
+
return Promise.resolve(unavailableBecause('ctx.remote is not injected'))
|
|
495
|
+
}
|
|
496
|
+
if (!remote) {
|
|
497
|
+
return Promise.resolve(unavailableBecause('the ctx.remote.settings namespace is absent'))
|
|
498
|
+
}
|
|
499
|
+
if (typeof remote.describe !== 'function') {
|
|
500
|
+
return Promise.resolve(unavailableBecause('ctx.remote.settings.describe is not a function'))
|
|
501
|
+
}
|
|
502
|
+
return Promise.resolve()
|
|
503
|
+
.then(function () {
|
|
504
|
+
return remote.describe()
|
|
505
|
+
})
|
|
506
|
+
.then(function (answer) {
|
|
507
|
+
// A Remote call answers with a `RemoteResult` envelope
|
|
508
|
+
// (`{ok:true, value}` / `{ok:false, error}`) and the client proxy
|
|
509
|
+
// does NOT unwrap it: reading `answer.namespaces` directly got
|
|
510
|
+
// `undefined`, which surfaced as the misleading
|
|
511
|
+
// "describe() returned no namespaces array" while the network
|
|
512
|
+
// exchange was perfectly healthy (ok:true, 20 namespaces).
|
|
513
|
+
var result = unwrapRemote(answer)
|
|
514
|
+
if (result.ok === false) {
|
|
515
|
+
return unavailableBecause('describe() failed: ' + result.reason)
|
|
516
|
+
}
|
|
517
|
+
var value = result.value
|
|
518
|
+
if (value === null || value === undefined) {
|
|
519
|
+
return unavailableBecause('describe() returned no value')
|
|
520
|
+
}
|
|
521
|
+
var list = Array.isArray(value.namespaces) ? value.namespaces : null
|
|
522
|
+
if (list === null) {
|
|
523
|
+
return unavailableBecause(
|
|
524
|
+
'describe() value has no namespaces array (keys: ' + Object.keys(value).join(', ') + ')',
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
var found = pickEntry(list)
|
|
528
|
+
if (found === undefined) {
|
|
529
|
+
var seen = list
|
|
530
|
+
.map(function (entry) {
|
|
531
|
+
return entry && entry.ns
|
|
532
|
+
})
|
|
533
|
+
.filter(Boolean)
|
|
534
|
+
.join(', ')
|
|
535
|
+
return unavailableBecause(
|
|
536
|
+
'no settings entry for this plugin (looked for "' +
|
|
537
|
+
NS +
|
|
538
|
+
'"; describe() returned: ' +
|
|
539
|
+
(seen || 'nothing') +
|
|
540
|
+
')',
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
resolvedNs = found.ns
|
|
544
|
+
return {
|
|
545
|
+
status: 'ready',
|
|
546
|
+
available: true,
|
|
547
|
+
// The provider's writability gates the whole form; a read-only
|
|
548
|
+
// deployment still shows the effective values.
|
|
549
|
+
writable: answer.writable !== false,
|
|
550
|
+
value: found.value || null,
|
|
551
|
+
revision: found.revision,
|
|
552
|
+
base: {},
|
|
553
|
+
}
|
|
554
|
+
}, function (failure) {
|
|
555
|
+
return unavailableBecause(
|
|
556
|
+
'describe() failed: ' + String((failure && failure.message) || failure),
|
|
557
|
+
)
|
|
558
|
+
})
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
available: remote !== undefined && typeof remote.describe === 'function',
|
|
563
|
+
/**
|
|
564
|
+
* @param ops - `SettingsPathOp` list, e.g. `{op:'set', path, value}`.
|
|
565
|
+
* @returns a promise resolving to the refreshed view.
|
|
566
|
+
*/
|
|
567
|
+
mutate: function (ops) {
|
|
568
|
+
if (!remote || typeof remote.mutate !== 'function') {
|
|
569
|
+
return Promise.reject(new Error('settings remote is unavailable in this deployment'))
|
|
570
|
+
}
|
|
571
|
+
// The revision is read fresh immediately before the write so a page
|
|
572
|
+
// left open across another edit reports a conflict instead of
|
|
573
|
+
// silently overwriting it. `read()` also refreshes `resolvedNs`.
|
|
574
|
+
return read().then(function (snap) {
|
|
575
|
+
if (!snap.available) {
|
|
576
|
+
throw new Error(snap.reason || 'this plugin has no settings entry in the active profile')
|
|
577
|
+
}
|
|
578
|
+
if (resolvedNs === null) {
|
|
579
|
+
throw new Error('this plugin did not resolve its settings entry id')
|
|
580
|
+
}
|
|
581
|
+
// The write answers with a RemoteResult envelope as well; unwrap
|
|
582
|
+
// it so a refusal surfaces as a real message instead of looking
|
|
583
|
+
// like a successful save.
|
|
584
|
+
return remote.mutate(resolvedNs, ops, snap.revision).then(function (answer) {
|
|
585
|
+
var result = unwrapRemote(answer)
|
|
586
|
+
if (result.ok === false) throw new Error(result.reason)
|
|
587
|
+
return result.value
|
|
588
|
+
})
|
|
589
|
+
})
|
|
590
|
+
},
|
|
591
|
+
/**
|
|
592
|
+
* @param listener - called after every host-side change to this entry.
|
|
593
|
+
* @returns an unsubscribe function.
|
|
594
|
+
*/
|
|
595
|
+
subscribe: function (listener) {
|
|
596
|
+
// The event surface lives on `ctx.remote` itself, not on the
|
|
597
|
+
// namespace: a shipped settings page listens as
|
|
598
|
+
// `ctx.remote.$on('credentials/reference-updated', …)` while
|
|
599
|
+
// declaring `inject: ['remote', 'remote.<ns>']`.
|
|
600
|
+
var events = ctx && ctx.remote
|
|
601
|
+
if (!events || typeof events.$on !== 'function') return function () {}
|
|
602
|
+
try {
|
|
603
|
+
return events.$on('settings/document-updated', function (ns) {
|
|
604
|
+
// Match the resolved entry id, not the package-relative name.
|
|
605
|
+
if (ns === undefined || ns === resolvedNs || ns === NS) listener()
|
|
606
|
+
})
|
|
607
|
+
} catch (error) {
|
|
608
|
+
// A missing gateway listener must not take the page down.
|
|
609
|
+
return function () {}
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
read: read,
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
348
616
|
/** @param props - the section owner props plus the plugin context. */
|
|
349
617
|
function SettingsSection(props) {
|
|
350
618
|
var ctx = props.ctx
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
var
|
|
356
|
-
|
|
357
|
-
|
|
619
|
+
// A host that can answer synchronously may hand the view over directly;
|
|
620
|
+
// against the real shell this is undefined and the page reads the
|
|
621
|
+
// settings Remote instead.
|
|
622
|
+
var seed = normaliseSnapshot(props.settingsSnapshot)
|
|
623
|
+
var sourceRef = useRef(null)
|
|
624
|
+
if (sourceRef.current === null) sourceRef.current = createSettingsSource(ctx)
|
|
625
|
+
var source = sourceRef.current
|
|
626
|
+
|
|
627
|
+
var snapState = useState(seed)
|
|
358
628
|
var snap = snapState[0]
|
|
359
629
|
var setSnap = snapState[1]
|
|
360
630
|
|
|
@@ -372,23 +642,39 @@
|
|
|
372
642
|
|
|
373
643
|
useEffect(
|
|
374
644
|
function () {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
645
|
+
var alive = true
|
|
646
|
+
function refresh() {
|
|
647
|
+
source.read().then(
|
|
648
|
+
function (next) {
|
|
649
|
+
if (alive) setSnap(next)
|
|
650
|
+
},
|
|
651
|
+
function (failure) {
|
|
652
|
+
if (alive) setError(String((failure && failure.message) || failure))
|
|
653
|
+
},
|
|
654
|
+
)
|
|
655
|
+
}
|
|
656
|
+
// A supplied snapshot is already the current view; only the Remote
|
|
657
|
+
// path needs the initial round trip.
|
|
658
|
+
if (seed === null) refresh()
|
|
659
|
+
var off = source.subscribe(refresh)
|
|
660
|
+
return function () {
|
|
661
|
+
alive = false
|
|
662
|
+
off()
|
|
663
|
+
}
|
|
378
664
|
},
|
|
379
|
-
[
|
|
665
|
+
[source],
|
|
380
666
|
)
|
|
381
667
|
|
|
382
668
|
// Re-seed the editor from every accepted snapshot, but never over an
|
|
383
669
|
// unsaved edit: the user's typing outranks a background refresh.
|
|
384
|
-
var revision = snap.revision
|
|
670
|
+
var revision = snap ? snap.revision : undefined
|
|
385
671
|
useEffect(
|
|
386
672
|
function () {
|
|
387
|
-
if (dirtyRef.current) return
|
|
673
|
+
if (dirtyRef.current || snap === null) return
|
|
388
674
|
var value = snap.value || {}
|
|
389
675
|
setDraft({ hooks: toLines(value.hooks), serialHooks: toLines(value.serialHooks), log: value.log || '' })
|
|
390
676
|
},
|
|
391
|
-
[revision, snap
|
|
677
|
+
[revision, snap],
|
|
392
678
|
)
|
|
393
679
|
|
|
394
680
|
/** @param fieldName - settings field to edit; @param next - its new textarea content. */
|
|
@@ -417,7 +703,7 @@
|
|
|
417
703
|
}
|
|
418
704
|
|
|
419
705
|
function save() {
|
|
420
|
-
var value = snap.value || {}
|
|
706
|
+
var value = (snap && snap.value) || {}
|
|
421
707
|
var ops = []
|
|
422
708
|
if (!sameLines(value.hooks, draft.hooks)) {
|
|
423
709
|
ops.push({ op: 'set', path: ['hooks'], value: fromLines(draft.hooks) })
|
|
@@ -429,13 +715,13 @@
|
|
|
429
715
|
ops.push({ op: 'set', path: ['log'], value: draft.log })
|
|
430
716
|
}
|
|
431
717
|
if (ops.length === 0) return
|
|
432
|
-
settle(
|
|
718
|
+
settle(source.mutate(ops))
|
|
433
719
|
}
|
|
434
720
|
|
|
435
|
-
/** Send every field back to the composition
|
|
721
|
+
/** Send every field back to the composition default (the schema default). */
|
|
436
722
|
function reset() {
|
|
437
723
|
settle(
|
|
438
|
-
|
|
724
|
+
source.mutate([
|
|
439
725
|
{ op: 'unset', path: ['hooks'] },
|
|
440
726
|
{ op: 'unset', path: ['serialHooks'] },
|
|
441
727
|
{ op: 'unset', path: ['log'] },
|
|
@@ -443,7 +729,9 @@
|
|
|
443
729
|
)
|
|
444
730
|
}
|
|
445
731
|
|
|
446
|
-
|
|
732
|
+
// Unknown until the descriptor arrives; a read-only provider disables
|
|
733
|
+
// the whole form rather than letting the user type into a dead editor.
|
|
734
|
+
var writable = snap !== null && snap.writable
|
|
447
735
|
|
|
448
736
|
/**
|
|
449
737
|
* @param label - field caption.
|
|
@@ -496,37 +784,44 @@
|
|
|
496
784
|
)
|
|
497
785
|
}
|
|
498
786
|
|
|
499
|
-
var
|
|
500
|
-
!sameLines(snap.value && snap.value.hooks, draft.hooks) ||
|
|
501
|
-
!sameLines(snap.value && snap.value.serialHooks, draft.serialHooks) ||
|
|
502
|
-
((snap.value && snap.value.log) || '') !== draft.log
|
|
787
|
+
var current = (snap && snap.value) || {}
|
|
503
788
|
|
|
504
|
-
var
|
|
789
|
+
var dirty =
|
|
790
|
+
!sameLines(current.hooks, draft.hooks) ||
|
|
791
|
+
!sameLines(current.serialHooks, draft.serialHooks) ||
|
|
792
|
+
(current.log || '') !== draft.log
|
|
793
|
+
|
|
794
|
+
// Three states: the descriptor has not arrived yet, this deployment has
|
|
795
|
+
// no settings entry for the plugin, or the form is ready.
|
|
796
|
+
var body
|
|
797
|
+
if (snap === null) {
|
|
798
|
+
body = e('p', null, t('loading'))
|
|
799
|
+
} else if (!snap.available) {
|
|
800
|
+
// Print the concrete cause next to the headline: the generic sentence
|
|
801
|
+
// alone could not tell a wiring fault from an unmounted host half.
|
|
802
|
+
body = e(
|
|
803
|
+
'div',
|
|
804
|
+
null,
|
|
805
|
+
e('p', null, t('unavailable')),
|
|
806
|
+
snap.reason ? e('p', { className: 'ho-note' }, String(snap.reason)) : null,
|
|
807
|
+
)
|
|
808
|
+
} else {
|
|
809
|
+
body = e(
|
|
810
|
+
'div',
|
|
811
|
+
{ className: CLASS },
|
|
812
|
+
renderListField(t('fieldHooks'), 'hooks', draft.hooks, 8),
|
|
813
|
+
renderListField(t('fieldSerialHooks'), 'serialHooks', draft.serialHooks, 3),
|
|
814
|
+
renderLineField(t('fieldLog'), 'log', draft.log),
|
|
815
|
+
)
|
|
816
|
+
}
|
|
505
817
|
|
|
506
818
|
return e(
|
|
507
819
|
'div',
|
|
508
820
|
{ className: CLASS },
|
|
509
821
|
e('h3', null, t('nav')),
|
|
510
822
|
e('p', null, t('intro')),
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
: snap.status === 'unavailable'
|
|
514
|
-
? e('p', null, t('unavailable'))
|
|
515
|
-
: e(
|
|
516
|
-
'div',
|
|
517
|
-
{ className: CLASS },
|
|
518
|
-
renderListField(t('fieldHooks'), 'hooks', draft.hooks, 8),
|
|
519
|
-
renderListField(t('fieldSerialHooks'), 'serialHooks', draft.serialHooks, 3),
|
|
520
|
-
renderLineField(t('fieldLog'), 'log', draft.log),
|
|
521
|
-
),
|
|
522
|
-
e(
|
|
523
|
-
'p',
|
|
524
|
-
{ className: 'ho-note' },
|
|
525
|
-
t('note', {
|
|
526
|
-
hooks: String(Array.isArray(base.hooks) ? base.hooks.length : 0),
|
|
527
|
-
serial: String(Array.isArray(base.serialHooks) ? base.serialHooks.length : 0),
|
|
528
|
-
}),
|
|
529
|
-
),
|
|
823
|
+
body,
|
|
824
|
+
e('p', { className: 'ho-note' }, t('note')),
|
|
530
825
|
error ? saveFailedStatus(error) : null,
|
|
531
826
|
e(
|
|
532
827
|
'div',
|
|
@@ -546,7 +841,7 @@
|
|
|
546
841
|
}
|
|
547
842
|
|
|
548
843
|
/**
|
|
549
|
-
* @param message - the failure text reported by
|
|
844
|
+
* @param message - the failure text reported by the settings write.
|
|
550
845
|
* @returns the save-failure status: the shell's StateDot + Tag when the kit is
|
|
551
846
|
* there, the plugin's own chip when it is not.
|
|
552
847
|
*/
|
|
@@ -560,11 +855,12 @@
|
|
|
560
855
|
}
|
|
561
856
|
|
|
562
857
|
// ── plugin ─────────────────────────────────────────────────────────────
|
|
563
|
-
// '
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
|
|
858
|
+
// 'remote.settings' is a required service: the settings Remote namespace
|
|
859
|
+
// is how a 0.1.7 page reads and writes its entry. Without it the page is
|
|
860
|
+
// not registered at all, rather than rendering an editor that cannot save.
|
|
861
|
+
// 0.1.6 exposed `settingsScope` instead; that service no longer exists, so
|
|
862
|
+
// naming it here would leave this plugin permanently pending.
|
|
863
|
+
var inject = ['slots', 'remote', 'remote.settings', 'locale']
|
|
568
864
|
|
|
569
865
|
/** @param ctx - the client plugin context. */
|
|
570
866
|
function apply(ctx) {
|
|
@@ -599,6 +895,8 @@
|
|
|
599
895
|
exports.apply = apply
|
|
600
896
|
exports.inject = inject
|
|
601
897
|
exports.NS = NS
|
|
898
|
+
exports.pickEntry = pickEntry
|
|
899
|
+
exports.unwrapRemote = unwrapRemote
|
|
602
900
|
exports.CLASS = CLASS
|
|
603
901
|
exports.CSS = CSS
|
|
604
902
|
exports.ZH = zh
|
package/lib/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { a as Phase, i as HookOrderingLogConfig, n as HookControlError } from ".
|
|
|
4
4
|
import { n as HookOrdering, r as HookOrderingConfig, t as HookEntry } from "./waterfall-_5HkptkS.js";
|
|
5
5
|
import { n as SerialHookOrdering, r as SerialHookOrderingConfig, t as SerialHookEntry } from "./serial-D8ZJCBKL.js";
|
|
6
6
|
import { Context } from "@deepseek-ai/cordis";
|
|
7
|
+
import * as _deepseek_ai_schemastery0 from "@deepseek-ai/schemastery";
|
|
7
8
|
|
|
8
9
|
//#region src/dsh.d.ts
|
|
9
10
|
|
|
@@ -75,8 +76,23 @@ declare const DEFAULT_WATERFALL_HOOKS: readonly string[];
|
|
|
75
76
|
declare const DEFAULT_SYNC_RETURN_HOOKS: readonly string[];
|
|
76
77
|
/** The dsh serial hook controlled by default. */
|
|
77
78
|
declare const DEFAULT_SERIAL_HOOKS: readonly string[];
|
|
78
|
-
/**
|
|
79
|
-
|
|
79
|
+
/**
|
|
80
|
+
* This plugin's Cordis Config schema.
|
|
81
|
+
*
|
|
82
|
+
* Since DSH 0.1.7 this *is* the Settings page: the fields are `.volatile()`, so
|
|
83
|
+
* the settings service projects exactly them into an editable form and commits
|
|
84
|
+
* an edit into the live reference rather than remounting the plugin. The
|
|
85
|
+
* defaults are the real hook sets below, so an entry whose `config:` key is
|
|
86
|
+
* empty resolves to them.
|
|
87
|
+
*/
|
|
88
|
+
declare const Config: _deepseek_ai_schemastery0.default;
|
|
89
|
+
/**
|
|
90
|
+
* Plugin config as a plain value.
|
|
91
|
+
*
|
|
92
|
+
* A loader-resolved config carries the volatile fields as references instead of
|
|
93
|
+
* plain arrays, so this describes the *plain* shape callers and tests construct.
|
|
94
|
+
*/
|
|
95
|
+
interface ConfigShape {
|
|
80
96
|
/**
|
|
81
97
|
* Waterfall hooks to control. Defaults to {@link DEFAULT_WATERFALL_HOOKS}.
|
|
82
98
|
* Pass `[]` to disable the waterfall service entirely.
|
|
@@ -92,10 +108,16 @@ interface Config {
|
|
|
92
108
|
* therefore refuse participants. Defaults to {@link DEFAULT_SYNC_RETURN_HOOKS}.
|
|
93
109
|
* Pass `[]` — or a set without a given hook — to opt in to ordering it, once
|
|
94
110
|
* the host awaits that dispatch.
|
|
111
|
+
*
|
|
112
|
+
* Composition-only: this describes the HOST's dispatch, not a user
|
|
113
|
+
* preference, so it is intentionally not a field of {@link Config} and never
|
|
114
|
+
* appears on the Settings page.
|
|
95
115
|
*/
|
|
96
116
|
syncReturnHooks?: readonly string[];
|
|
97
117
|
/** When set, the constraint DAG (JSON) is logged to this file on every change. */
|
|
98
118
|
log?: string;
|
|
119
|
+
/** Overrides the loader entry id; only non-loader harnesses need it. */
|
|
120
|
+
entryId?: string;
|
|
99
121
|
}
|
|
100
122
|
/**
|
|
101
123
|
* Mount {@link HookOrdering} and/or {@link SerialHookOrdering} and control the
|
|
@@ -107,15 +129,23 @@ interface Config {
|
|
|
107
129
|
* package's own `cordis.patch.yml` is written exactly that way — every option
|
|
108
130
|
* commented out — so a null config must mean "all defaults", not a crash.
|
|
109
131
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
132
|
+
* Since DSH 0.1.7 there is no second "user layer": the row's `config` IS the
|
|
133
|
+
* editable settings, because Cordis validates it against {@link Config} and the
|
|
134
|
+
* settings service projects that schema. A volatile field arrives here as a
|
|
135
|
+
* reference rather than a plain array, which is why every read goes through
|
|
136
|
+
* {@link readSetting} — that also keeps a plain Cordis mount (and an older
|
|
137
|
+
* harness) working, where the fields are plain values.
|
|
138
|
+
*
|
|
139
|
+
* `hooks`/`serialHooks` are read once, at apply time. An edit from the Settings
|
|
140
|
+
* page therefore needs a plugin reload to take effect — the same restart
|
|
141
|
+
* semantics this namespace declared before 0.1.7 (`applies: 'restart'`), since
|
|
142
|
+
* changing the controlled set means adding or removing live hook brackets and
|
|
143
|
+
* the coordinator has no public "release one hook" operation.
|
|
114
144
|
*
|
|
115
145
|
* @param ctx - the Cordis context.
|
|
116
146
|
* @param config - which hooks to control and an optional DAG `log` file; null means all defaults.
|
|
117
147
|
*/
|
|
118
|
-
declare function apply(ctx: Context, config?:
|
|
148
|
+
declare function apply(ctx: Context, config?: ConfigShape | null): void;
|
|
119
149
|
//#endregion
|
|
120
|
-
export { type
|
|
150
|
+
export { Config, type ConfigShape, DEFAULT_SERIAL_HOOKS, DEFAULT_SYNC_RETURN_HOOKS, DEFAULT_WATERFALL_HOOKS, type Dag, type DagEdge, type DagSection, type DagSectionGraph, DuplicateNameError, HookControlError, type HookEntry, HookOrdering, type HookOrderingConfig, type HookOrderingLogConfig, type Orderable, OrderingCycleError, type Phase, type SerialHookEntry, SerialHookOrdering, type SerialHookOrderingConfig, apply, buildDag, inject, name, topoSort };
|
|
121
151
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/dsh.ts"],"sourcesContent":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/dsh.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;cAuBa,IAAA;;;;;;;;;;;;;;;;;;cAmBA;;;;;;;;;;;;;;;;;;;cAoBA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAuCA;;cAOA;;;;;;;;;;cAWA,QAA2E,yBAAA,CAArE;;;;;;;UAWF,WAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsDD,KAAA,MAAW,kBAAiB"}
|
package/lib/index.js
CHANGED
|
@@ -6,51 +6,43 @@ import { t as SerialHookOrdering } from "./serial-Cu7usHjI.js";
|
|
|
6
6
|
import Schema from "@deepseek-ai/schemastery";
|
|
7
7
|
|
|
8
8
|
//#region src/settings.ts
|
|
9
|
-
/** Namespace name — dsh requires a lowercase hyphenated identifier. */
|
|
10
|
-
const SETTINGS_NS = "hooks-ordering";
|
|
11
9
|
/**
|
|
12
|
-
*
|
|
10
|
+
* Build the Config schema from the caller's default hook lists.
|
|
13
11
|
*
|
|
14
|
-
* Every field
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* Every field is `.volatile()`, so every field is user-editable from the
|
|
13
|
+
* Settings page and an edit is committed into the live reference instead of
|
|
14
|
+
* remounting the plugin. `log` is volatile too, because a non-volatile field
|
|
15
|
+
* would not appear in the form at all.
|
|
16
|
+
*
|
|
17
|
+
* @param defaultWaterfallHooks - waterfall hooks controlled when unconfigured.
|
|
18
|
+
* @param defaultSerialHooks - serial hooks controlled when unconfigured.
|
|
19
|
+
* @returns a callable schemastery schema, which is what Cordis resolves through.
|
|
18
20
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
function hooksOrderingConfig(defaultWaterfallHooks, defaultSerialHooks) {
|
|
22
|
+
return Schema.object({
|
|
23
|
+
hooks: Schema.array(Schema.string()).default([...defaultWaterfallHooks]).volatile(),
|
|
24
|
+
serialHooks: Schema.array(Schema.string()).default([...defaultSerialHooks]).volatile(),
|
|
25
|
+
log: Schema.string().default("").volatile()
|
|
26
|
+
});
|
|
27
|
+
}
|
|
24
28
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* @param ctx - the Cordis context to look the service up on.
|
|
37
|
-
* @param base - the composition layer to register as the base value.
|
|
38
|
-
* @returns the resolved settings, or `undefined` when the provider is missing
|
|
39
|
-
* (which `inject: ['settings']` rules out in dsh — the guard is defensive) or
|
|
40
|
-
* registration failed — the caller then uses `base`.
|
|
29
|
+
* Read one resolved field, unwrapping a volatile reference when the loader
|
|
30
|
+
* supplied one and falling back to a default when the field is absent.
|
|
31
|
+
*
|
|
32
|
+
* A loader-provided Config carries volatile fields as references (`.get()`);
|
|
33
|
+
* a plain Cordis mount, or an older harness, passes plain values. Both must
|
|
34
|
+
* work, and an absent field must not be confused with an explicit empty one.
|
|
35
|
+
*
|
|
36
|
+
* @param config - the resolved plugin config (possibly `null`).
|
|
37
|
+
* @param field - the field to read.
|
|
38
|
+
* @param fallback - value to use when the field is absent.
|
|
39
|
+
* @returns the field's current value, or `fallback`.
|
|
41
40
|
*/
|
|
42
|
-
function
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
base,
|
|
48
|
-
applies: "restart"
|
|
49
|
-
}).get();
|
|
50
|
-
} catch (error) {
|
|
51
|
-
console.warn("hooks-ordering: settings registration failed; falling back to the composition config:", error);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
41
|
+
function readSetting(config, field, fallback) {
|
|
42
|
+
const value = config?.[field];
|
|
43
|
+
if (value === void 0 || value === null) return fallback;
|
|
44
|
+
if (typeof value.get === "function") return value.get() ?? fallback;
|
|
45
|
+
return value;
|
|
54
46
|
}
|
|
55
47
|
|
|
56
48
|
//#endregion
|
|
@@ -139,6 +131,16 @@ const DEFAULT_SYNC_RETURN_HOOKS = [
|
|
|
139
131
|
/** The dsh serial hook controlled by default. */
|
|
140
132
|
const DEFAULT_SERIAL_HOOKS = ["agent/turn-stopping"];
|
|
141
133
|
/**
|
|
134
|
+
* This plugin's Cordis Config schema.
|
|
135
|
+
*
|
|
136
|
+
* Since DSH 0.1.7 this *is* the Settings page: the fields are `.volatile()`, so
|
|
137
|
+
* the settings service projects exactly them into an editable form and commits
|
|
138
|
+
* an edit into the live reference rather than remounting the plugin. The
|
|
139
|
+
* defaults are the real hook sets below, so an entry whose `config:` key is
|
|
140
|
+
* empty resolves to them.
|
|
141
|
+
*/
|
|
142
|
+
const Config = hooksOrderingConfig(DEFAULT_WATERFALL_HOOKS, DEFAULT_SERIAL_HOOKS);
|
|
143
|
+
/**
|
|
142
144
|
* Mount {@link HookOrdering} and/or {@link SerialHookOrdering} and control the
|
|
143
145
|
* configured hooks once the services are active.
|
|
144
146
|
*
|
|
@@ -148,22 +150,29 @@ const DEFAULT_SERIAL_HOOKS = ["agent/turn-stopping"];
|
|
|
148
150
|
* package's own `cordis.patch.yml` is written exactly that way — every option
|
|
149
151
|
* commented out — so a null config must mean "all defaults", not a crash.
|
|
150
152
|
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
153
|
+
* Since DSH 0.1.7 there is no second "user layer": the row's `config` IS the
|
|
154
|
+
* editable settings, because Cordis validates it against {@link Config} and the
|
|
155
|
+
* settings service projects that schema. A volatile field arrives here as a
|
|
156
|
+
* reference rather than a plain array, which is why every read goes through
|
|
157
|
+
* {@link readSetting} — that also keeps a plain Cordis mount (and an older
|
|
158
|
+
* harness) working, where the fields are plain values.
|
|
159
|
+
*
|
|
160
|
+
* `hooks`/`serialHooks` are read once, at apply time. An edit from the Settings
|
|
161
|
+
* page therefore needs a plugin reload to take effect — the same restart
|
|
162
|
+
* semantics this namespace declared before 0.1.7 (`applies: 'restart'`), since
|
|
163
|
+
* changing the controlled set means adding or removing live hook brackets and
|
|
164
|
+
* the coordinator has no public "release one hook" operation.
|
|
155
165
|
*
|
|
156
166
|
* @param ctx - the Cordis context.
|
|
157
167
|
* @param config - which hooks to control and an optional DAG `log` file; null means all defaults.
|
|
158
168
|
*/
|
|
159
169
|
function apply(ctx, config = {}) {
|
|
160
170
|
const row = config ?? {};
|
|
161
|
-
const
|
|
162
|
-
hooks: row
|
|
163
|
-
serialHooks: row
|
|
164
|
-
log: row
|
|
171
|
+
const { hooks, serialHooks, log } = {
|
|
172
|
+
hooks: readSetting(row, "hooks", DEFAULT_WATERFALL_HOOKS),
|
|
173
|
+
serialHooks: readSetting(row, "serialHooks", DEFAULT_SERIAL_HOOKS),
|
|
174
|
+
log: readSetting(row, "log", "")
|
|
165
175
|
};
|
|
166
|
-
const { hooks, serialHooks, log } = resolveSettings(ctx, base) ?? base;
|
|
167
176
|
const serviceConfig = log === "" ? {} : { log };
|
|
168
177
|
const syncReturnHooks = row.syncReturnHooks ?? DEFAULT_SYNC_RETURN_HOOKS;
|
|
169
178
|
const deps = [];
|
|
@@ -186,5 +195,5 @@ function apply(ctx, config = {}) {
|
|
|
186
195
|
}
|
|
187
196
|
|
|
188
197
|
//#endregion
|
|
189
|
-
export { DEFAULT_SERIAL_HOOKS, DEFAULT_SYNC_RETURN_HOOKS, DEFAULT_WATERFALL_HOOKS, DuplicateNameError, HookControlError, HookOrdering, OrderingCycleError, SerialHookOrdering, apply, buildDag, inject, name, topoSort };
|
|
198
|
+
export { Config, DEFAULT_SERIAL_HOOKS, DEFAULT_SYNC_RETURN_HOOKS, DEFAULT_WATERFALL_HOOKS, DuplicateNameError, HookControlError, HookOrdering, OrderingCycleError, SerialHookOrdering, apply, buildDag, inject, name, topoSort };
|
|
190
199
|
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["DEFAULT_WATERFALL_HOOKS: readonly string[]","DEFAULT_SYNC_RETURN_HOOKS: readonly string[]","DEFAULT_SERIAL_HOOKS: readonly string[]","base: HooksOrderingSettings","deps: string[]","HookOrdering"],"sources":["../src/settings.ts","../src/dsh.ts"],"sourcesContent":["/**\n * The dsh settings namespace for this plugin: the three fields a user can edit\n * from the Settings page instead of a `cordis.patch.yml` row.\n *\n * Two constraints are load-bearing and easy to get wrong:\n *\n * - The schema handed to `settings.register` must be a **callable** schemastery\n * schema. dsh resolves a namespace by calling `schema(merged)` and reads\n * `schema.toJSON()` for the form, so a plain object throws\n * `schema is not a function` — during plugin assembly, which takes the whole\n * profile down rather than failing one form.\n * - That is also why registration is wrapped in `try/catch` here: an optional\n * settings form must never be able to stop the harness from booting.\n *\n * @module dsh-hooks-ordering/settings\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport Schema from '@deepseek-ai/schemastery'\n\n/** The three fields this plugin exposes for configuration. */\nexport interface HooksOrderingSettings {\n /** Waterfall hooks to control; `[]` disables the waterfall service entirely. */\n readonly hooks: readonly string[]\n /** Serial hooks to control; `[]` disables the serial service entirely. */\n readonly serialHooks: readonly string[]\n /** Constraint-DAG log file; the empty string means \"do not log\". */\n readonly log: string\n}\n\n/** Namespace name — dsh requires a lowercase hyphenated identifier. */\nexport const SETTINGS_NS = 'hooks-ordering'\n\n/**\n * Schema for {@link SETTINGS_NS}.\n *\n * Every field defaults to its \"off\" value. The *meaningful* defaults — the\n * hooks this plugin controls when nothing overrides them — are supplied by the\n * caller as the composition `base` layer (see {@link resolveSettings}), so a\n * namespace the user has never touched still resolves to a complete object.\n */\nexport const HooksOrderingSettingsSchema = Schema.object({\n hooks: Schema.array(Schema.string()).default([]),\n serialHooks: Schema.array(Schema.string()).default([]),\n log: Schema.string().default(''),\n})\n\n/**\n * The slice of dsh's `settings` service this plugin uses.\n *\n * Structural rather than imported from `@deepseek-ai/dsh-settings`: it describes\n * exactly the contract this plugin relies on without adding a build-time\n * dependency on a dsh package, whose internals are still 0.1.x-rc.\n */\nexport interface SettingsServiceLike {\n /**\n * @param ns - the namespace to register; must be a lowercase hyphenated identifier.\n * @param schema - a callable schemastery schema, not a plain object.\n * @param options - `base` is the composition layer, `applies` says whether an\n * edit takes effect live or needs a restart.\n * @returns a scope whose `get()` is the resolved value (schema defaults, then `base`, then the user layer).\n */\n register(\n ns: string,\n schema: unknown,\n options?: { readonly base?: unknown; readonly applies?: 'live' | 'restart' },\n ): { get(): HooksOrderingSettings }\n}\n\n/**\n * Register the settings namespace and read its resolved value.\n *\n * `base` is the composition layer — what the row (or the built-in defaults)\n * asks for — so the user layer edits *over* it and a reset returns to it rather\n * than to an empty form.\n *\n * `applies: 'restart'` is deliberate. Changing `hooks`/`serialHooks` means\n * installing or removing bracket listeners on live hooks, and the coordinator\n * has no public \"release one hook\" operation; a restart applies the change\n * cleanly instead of re-wiring the dispatch chain mid-flight.\n *\n * @param ctx - the Cordis context to look the service up on.\n * @param base - the composition layer to register as the base value.\n * @returns the resolved settings, or `undefined` when the provider is missing\n * (which `inject: ['settings']` rules out in dsh — the guard is defensive) or\n * registration failed — the caller then uses `base`.\n */\nexport function resolveSettings(ctx: Context, base: HooksOrderingSettings): HooksOrderingSettings | undefined {\n const settings = ctx.get('settings') as SettingsServiceLike | undefined\n if (settings === undefined) return undefined\n try {\n return settings.register(SETTINGS_NS, HooksOrderingSettingsSchema, { base, applies: 'restart' }).get()\n } catch (error) {\n console.warn('hooks-ordering: settings registration failed; falling back to the composition config:', error)\n return undefined\n }\n}\n","/**\n * The DeepSeek-Harness layer: a dsh plugin that mounts the ordering services\n * and takes control of the real dsh hooks that multiple independent packages\n * contribute to, so a profile can opt into deterministic ordering with one row.\n *\n * dsh (deepseek-harness) ships waterfall hooks such as `agent/pre-step`\n * (subscribed by a dozen+ independent packages), `tools/post-execute`,\n * `llm/stream`, and `system-prompt/assemble`, plus the serial hook\n * `agent/turn-stopping`. Their relative listener order is load-bearing yet\n * today decided only by binary `prepend` and registration timing. This plugin\n * controls those hooks up front; controlling an empty hook is a transparent\n * pass-through, so nothing changes until participants register with\n * `before`/`after`.\n *\n * @module dsh-hooks-ordering/dsh\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport HookOrdering from './waterfall.ts'\nimport { SerialHookOrdering } from './serial.ts'\nimport { type HooksOrderingSettings, resolveSettings } from './settings.ts'\n\n/** Cordis plugin name used by loader diagnostics. */\nexport const name = 'hooks-ordering'\n\n/**\n * `settings` is a **hard** dependency, and it has to be declared rather than\n * probed.\n *\n * cordis activates a plugin as soon as the services it declares exist, and\n * `ctx.get('settings')` reads the service store *without* creating that\n * requirement. Without this line the dsh layer loads in the first wave — before\n * the host has provided `settings` — the probe returns `undefined`, and the\n * settings namespace is never registered. Silently: no form, no error.\n *\n * Declaring it makes cordis wait, which also lands this plugin's prepended\n * brackets later in the boot, exactly where the ordering guarantee wants them.\n *\n * This entry targets dsh only — the hook names below are dsh's, and dsh always\n * provides `settings`, so the dependency costs nothing here. `/waterfall` and\n * `/serial` are the entries that carry no host-service requirement.\n */\nexport const inject = ['settings']\n\n/**\n * The dsh waterfall hooks this plugin controls by default — the ones multiple\n * independent packages contribute to, where relative order matters. Controlling\n * a hook with no registered participants is a no-op pass-through.\n *\n * Every name here was checked against the events dsh actually declares: each is\n * a `@mode waterfall` event in the installed build, and each is dispatched with\n * `await`, so the caller already handles a Promise and the bracket's async\n * phases cannot change the hook's return type. `tools/code-dispatch-log` used\n * to sit in this list and no dsh package declares it at all — controlling a\n * name nothing dispatches \"succeeds\" and then does nothing forever, which is\n * exactly the dead configuration this list must not carry.\n *\n * The other admission rule is {@link DEFAULT_SYNC_RETURN_HOOKS}: a hook whose\n * value the caller consumes without awaiting cannot carry ordered participants,\n * so controlling it by default would install a bracket nothing may ever\n * register into — the same dead configuration by a different route.\n */\nexport const DEFAULT_WATERFALL_HOOKS: readonly string[] = [\n 'agent/pre-step',\n 'agent/request',\n 'agent/request-error',\n 'system-prompt/assemble',\n 'tools/pre-execute',\n 'tools/execute',\n 'tools/post-execute',\n 'fs/write-intent',\n 'fs/edit-intent',\n 'approval/request',\n]\n\n/**\n * dsh hooks whose dispatch return value is consumed by the caller without\n * awaiting, and which therefore cannot carry ordered participants. Verified\n * against the installed build:\n *\n * - `llm/stream` — `dsh-llm` dispatches it as\n * `return this.ctx.waterfall(this, \"llm/stream\", options, …)` (no await), and\n * `dsh-session-title-llm` iterates the result with\n * `for await (const chunk of ctx.llm.stream(options))`. A Promise there throws\n * \"not async iterable\". It is a genuine multi-contributor hook (dsh-agent-loop,\n * dsh-llm's own invariant, dsh-session-checkpoint-policy, dsh-session-title),\n * so it is a real loss — but ordering it requires dsh to await the dispatch,\n * not a plugin-side workaround.\n * - `session-telemetry/record` — `dsh-session-telemetry` returns the record\n * straight out of the waterfall and hands it to the backend, so a Promise\n * would be emitted as a record: silent corruption, no error anywhere.\n * - `compaction/summary-error` — `dsh-compaction-basic` dispatches it as\n * `recover: (…) => this.ctx.waterfall(this, \"compaction/summary-error\", …, () => false)`\n * (no await) and consumes the boolean synchronously in\n * `if (!dependencies.recover(error, agent, prepared.shadowedSeqs, signal)) throw error`.\n * A Promise is always truthy, so `!recover(…)` is always false and every\n * summarizer failure is swallowed instead of rethrown — the compaction then\n * proceeds as if recovery had succeeded.\n *\n * Override per profile with `syncReturnHooks` once the host awaits one of them.\n */\nexport const DEFAULT_SYNC_RETURN_HOOKS: readonly string[] = [\n 'llm/stream',\n 'session-telemetry/record',\n 'compaction/summary-error',\n]\n\n/** The dsh serial hook controlled by default. */\nexport const DEFAULT_SERIAL_HOOKS: readonly string[] = ['agent/turn-stopping']\n\n/** Plugin config. */\nexport interface Config {\n /**\n * Waterfall hooks to control. Defaults to {@link DEFAULT_WATERFALL_HOOKS}.\n * Pass `[]` to disable the waterfall service entirely.\n */\n hooks?: readonly string[]\n /**\n * Serial hooks to control. Defaults to {@link DEFAULT_SERIAL_HOOKS}.\n * Pass `[]` to disable the serial service entirely.\n */\n serialHooks?: readonly string[]\n /**\n * Hooks whose return value the host consumes without awaiting, and which must\n * therefore refuse participants. Defaults to {@link DEFAULT_SYNC_RETURN_HOOKS}.\n * Pass `[]` — or a set without a given hook — to opt in to ordering it, once\n * the host awaits that dispatch.\n */\n syncReturnHooks?: readonly string[]\n /** When set, the constraint DAG (JSON) is logged to this file on every change. */\n log?: string\n}\n\n/**\n * Mount {@link HookOrdering} and/or {@link SerialHookOrdering} and control the\n * configured hooks once the services are active.\n *\n * `config` may be `null`, and that is the ordinary case rather than an edge\n * case: a loader row whose `config:` key is followed only by comments parses as\n * YAML null, and a default parameter covers `undefined` but not `null`. This\n * package's own `cordis.patch.yml` is written exactly that way — every option\n * commented out — so a null config must mean \"all defaults\", not a crash.\n *\n * The row is the *composition* layer of the settings namespace: the Settings\n * page edits a user layer over it, and a namespace the user has not touched\n * resolves back to this row (or to the built-in defaults). Without a settings\n * provider — plain Cordis — the row is the whole answer.\n *\n * @param ctx - the Cordis context.\n * @param config - which hooks to control and an optional DAG `log` file; null means all defaults.\n */\nexport function apply(ctx: Context, config: Config | null = {}): void {\n const row = config ?? {}\n const base: HooksOrderingSettings = {\n hooks: row.hooks ?? DEFAULT_WATERFALL_HOOKS,\n serialHooks: row.serialHooks ?? DEFAULT_SERIAL_HOOKS,\n log: row.log ?? '',\n }\n const { hooks, serialHooks, log } = resolveSettings(ctx, base) ?? base\n const serviceConfig = log === '' ? {} : { log }\n // Not part of the settings namespace: the sync-return list describes the\n // HOST's dispatch, not a user preference, so it is composition (row) only.\n const syncReturnHooks = row.syncReturnHooks ?? DEFAULT_SYNC_RETURN_HOOKS\n\n const deps: string[] = []\n if (hooks.length > 0) {\n ctx.plugin(HookOrdering, { ...serviceConfig, syncReturnHooks })\n deps.push('hooksOrdering')\n }\n if (serialHooks.length > 0) {\n ctx.plugin(SerialHookOrdering, serviceConfig)\n deps.push('serialHooksOrdering')\n }\n if (deps.length === 0) return\n\n // The services activate asynchronously; control the hooks once they exist.\n ctx.inject(deps, (ready) => {\n for (const hook of hooks) ready.hooksOrdering.control(hook)\n for (const hook of serialHooks) ready.serialHooksOrdering.control(hook)\n })\n}\n"],"mappings":";;;;;;;;;AA+BA,MAAa,cAAc;;;;;;;;;AAU3B,MAAa,8BAA8B,OAAO,OAAO;CACvD,OAAO,OAAO,MAAM,OAAO,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CAChD,aAAa,OAAO,MAAM,OAAO,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CACtD,KAAK,OAAO,QAAQ,CAAC,QAAQ,GAAG;CACjC,CAAC;;;;;;;;;;;;;;;;;;;AA0CF,SAAgB,gBAAgB,KAAc,MAAgE;CAC5G,MAAM,WAAW,IAAI,IAAI,WAAW;AACpC,KAAI,aAAa,OAAW,QAAO;AACnC,KAAI;AACF,SAAO,SAAS,SAAS,aAAa,6BAA6B;GAAE;GAAM,SAAS;GAAW,CAAC,CAAC,KAAK;UAC/F,OAAO;AACd,UAAQ,KAAK,yFAAyF,MAAM;AAC5G;;;;;;;ACvEJ,MAAa,OAAO;;;;;;;;;;;;;;;;;;AAmBpB,MAAa,SAAS,CAAC,WAAW;;;;;;;;;;;;;;;;;;;AAoBlC,MAAaA,0BAA6C;CACxD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BD,MAAaC,4BAA+C;CAC1D;CACA;CACA;CACD;;AAGD,MAAaC,uBAA0C,CAAC,sBAAsB;;;;;;;;;;;;;;;;;;;AA2C9E,SAAgB,MAAM,KAAc,SAAwB,EAAE,EAAQ;CACpE,MAAM,MAAM,UAAU,EAAE;CACxB,MAAMC,OAA8B;EAClC,OAAO,IAAI,SAAS;EACpB,aAAa,IAAI,eAAe;EAChC,KAAK,IAAI,OAAO;EACjB;CACD,MAAM,EAAE,OAAO,aAAa,QAAQ,gBAAgB,KAAK,KAAK,IAAI;CAClE,MAAM,gBAAgB,QAAQ,KAAK,EAAE,GAAG,EAAE,KAAK;CAG/C,MAAM,kBAAkB,IAAI,mBAAmB;CAE/C,MAAMC,OAAiB,EAAE;AACzB,KAAI,MAAM,SAAS,GAAG;AACpB,MAAI,OAAOC,mBAAc;GAAE,GAAG;GAAe;GAAiB,CAAC;AAC/D,OAAK,KAAK,gBAAgB;;AAE5B,KAAI,YAAY,SAAS,GAAG;AAC1B,MAAI,OAAO,oBAAoB,cAAc;AAC7C,OAAK,KAAK,sBAAsB;;AAElC,KAAI,KAAK,WAAW,EAAG;AAGvB,KAAI,OAAO,OAAO,UAAU;AAC1B,OAAK,MAAM,QAAQ,MAAO,OAAM,cAAc,QAAQ,KAAK;AAC3D,OAAK,MAAM,QAAQ,YAAa,OAAM,oBAAoB,QAAQ,KAAK;GACvE"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["DEFAULT_WATERFALL_HOOKS: readonly string[]","DEFAULT_SYNC_RETURN_HOOKS: readonly string[]","DEFAULT_SERIAL_HOOKS: readonly string[]","deps: string[]","HookOrdering"],"sources":["../src/settings.ts","../src/dsh.ts"],"sourcesContent":["/**\n * This plugin's Cordis Config — and, since DSH 0.1.7, its Settings page.\n *\n * 0.1.7 removed `ctx.settings.register(ns, schema)`. Settings are now *projected\n * from each entry's own Config*: the loader validates the row's `config` against\n * this schema, resolves the defaults, and the settings service renders exactly\n * the nodes marked `.volatile()` as an editable form. Three consequences are\n * load-bearing here:\n *\n * - The defaults below are the *real* defaults, not \"off\" placeholders. Nothing\n * supplies a second `base` layer any more, so an entry whose `config:` key is\n * empty must resolve to the hook set this plugin controls by default.\n * - `.volatile()` belongs on the individual fields, never on the root object:\n * a volatile root collapses the whole Config into a single reference and\n * discards every nested default (verified against schemastery 3.18.4), and\n * schemastery rejects a volatile node nested inside another volatile node.\n * - The schema is built lazily by {@link hooksOrderingConfig}. `dsh.ts` imports\n * this module and this module needs `dsh.ts`'s default hook lists, so building\n * the schema eagerly would read those bindings before they are initialized.\n *\n * `syncReturnHooks` is deliberately absent: it describes the HOST's dispatch\n * (which hooks are consumed without awaiting), not a user preference, so it\n * stays composition-only in `apply()`.\n *\n * @module dsh-hooks-ordering/settings\n */\n\nimport Schema from '@deepseek-ai/schemastery'\n\n/** The fields this plugin exposes as editable settings. */\nexport interface HooksOrderingSettings {\n /** Waterfall hooks to control; `[]` disables the waterfall service entirely. */\n readonly hooks: readonly string[]\n /** Serial hooks to control; `[]` disables the serial service entirely. */\n readonly serialHooks: readonly string[]\n /** Constraint-DAG log file; the empty string means \"do not log\". */\n readonly log: string\n}\n\n/**\n * Entry id used by `cordis.patch.yml` and, in 0.1.7, by the settings form.\n *\n * The form is keyed by the **loader entry id**; this constant is the shipped\n * row's id, and it is also the client half's settings-section id.\n */\nexport const SETTINGS_NS = 'hooks-ordering'\n\n/**\n * Build the Config schema from the caller's default hook lists.\n *\n * Every field is `.volatile()`, so every field is user-editable from the\n * Settings page and an edit is committed into the live reference instead of\n * remounting the plugin. `log` is volatile too, because a non-volatile field\n * would not appear in the form at all.\n *\n * @param defaultWaterfallHooks - waterfall hooks controlled when unconfigured.\n * @param defaultSerialHooks - serial hooks controlled when unconfigured.\n * @returns a callable schemastery schema, which is what Cordis resolves through.\n */\nexport function hooksOrderingConfig(\n defaultWaterfallHooks: readonly string[],\n defaultSerialHooks: readonly string[],\n): Schema {\n return Schema.object({\n hooks: Schema.array(Schema.string()).default([...defaultWaterfallHooks]).volatile(),\n serialHooks: Schema.array(Schema.string()).default([...defaultSerialHooks]).volatile(),\n log: Schema.string().default('').volatile(),\n })\n}\n\n/**\n * Read one resolved field, unwrapping a volatile reference when the loader\n * supplied one and falling back to a default when the field is absent.\n *\n * A loader-provided Config carries volatile fields as references (`.get()`);\n * a plain Cordis mount, or an older harness, passes plain values. Both must\n * work, and an absent field must not be confused with an explicit empty one.\n *\n * @param config - the resolved plugin config (possibly `null`).\n * @param field - the field to read.\n * @param fallback - value to use when the field is absent.\n * @returns the field's current value, or `fallback`.\n */\nexport function readSetting<T>(\n config: object | null | undefined,\n field: string,\n fallback: T,\n): T {\n const value = (config as Record<string, unknown> | null | undefined)?.[field]\n if (value === undefined || value === null) return fallback\n if (typeof (value as { get?: unknown }).get === 'function') {\n const current = (value as { get(): unknown }).get()\n return (current ?? fallback) as T\n }\n return value as T\n}\n","/**\n * The DeepSeek-Harness layer: a dsh plugin that mounts the ordering services\n * and takes control of the real dsh hooks that multiple independent packages\n * contribute to, so a profile can opt into deterministic ordering with one row.\n *\n * dsh (deepseek-harness) ships waterfall hooks such as `agent/pre-step`\n * (subscribed by a dozen+ independent packages), `tools/post-execute`,\n * `llm/stream`, and `system-prompt/assemble`, plus the serial hook\n * `agent/turn-stopping`. Their relative listener order is load-bearing yet\n * today decided only by binary `prepend` and registration timing. This plugin\n * controls those hooks up front; controlling an empty hook is a transparent\n * pass-through, so nothing changes until participants register with\n * `before`/`after`.\n *\n * @module dsh-hooks-ordering/dsh\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport HookOrdering from './waterfall.ts'\nimport { SerialHookOrdering } from './serial.ts'\nimport { type HooksOrderingSettings, hooksOrderingConfig, readSetting } from './settings.ts'\n\n/** Cordis plugin name used by loader diagnostics. */\nexport const name = 'hooks-ordering'\n\n/**\n * `settings` is a **hard** dependency, and it has to be declared rather than\n * probed.\n *\n * cordis activates a plugin as soon as the services it declares exist, and\n * `ctx.get('settings')` reads the service store *without* creating that\n * requirement. Without this line the dsh layer loads in the first wave — before\n * the host has provided `settings` — the probe returns `undefined`, and the\n * settings namespace is never registered. Silently: no form, no error.\n *\n * Declaring it makes cordis wait, which also lands this plugin's prepended\n * brackets later in the boot, exactly where the ordering guarantee wants them.\n *\n * This entry targets dsh only — the hook names below are dsh's, and dsh always\n * provides `settings`, so the dependency costs nothing here. `/waterfall` and\n * `/serial` are the entries that carry no host-service requirement.\n */\nexport const inject = ['settings']\n\n/**\n * The dsh waterfall hooks this plugin controls by default — the ones multiple\n * independent packages contribute to, where relative order matters. Controlling\n * a hook with no registered participants is a no-op pass-through.\n *\n * Every name here was checked against the events dsh actually declares: each is\n * a `@mode waterfall` event in the installed build, and each is dispatched with\n * `await`, so the caller already handles a Promise and the bracket's async\n * phases cannot change the hook's return type. `tools/code-dispatch-log` used\n * to sit in this list and no dsh package declares it at all — controlling a\n * name nothing dispatches \"succeeds\" and then does nothing forever, which is\n * exactly the dead configuration this list must not carry.\n *\n * The other admission rule is {@link DEFAULT_SYNC_RETURN_HOOKS}: a hook whose\n * value the caller consumes without awaiting cannot carry ordered participants,\n * so controlling it by default would install a bracket nothing may ever\n * register into — the same dead configuration by a different route.\n */\nexport const DEFAULT_WATERFALL_HOOKS: readonly string[] = [\n 'agent/pre-step',\n 'agent/request',\n 'agent/request-error',\n 'system-prompt/assemble',\n 'tools/pre-execute',\n 'tools/execute',\n 'tools/post-execute',\n 'fs/write-intent',\n 'fs/edit-intent',\n 'approval/request',\n]\n\n/**\n * dsh hooks whose dispatch return value is consumed by the caller without\n * awaiting, and which therefore cannot carry ordered participants. Verified\n * against the installed build:\n *\n * - `llm/stream` — `dsh-llm` dispatches it as\n * `return this.ctx.waterfall(this, \"llm/stream\", options, …)` (no await), and\n * `dsh-session-title-llm` iterates the result with\n * `for await (const chunk of ctx.llm.stream(options))`. A Promise there throws\n * \"not async iterable\". It is a genuine multi-contributor hook (dsh-agent-loop,\n * dsh-llm's own invariant, dsh-session-checkpoint-policy, dsh-session-title),\n * so it is a real loss — but ordering it requires dsh to await the dispatch,\n * not a plugin-side workaround.\n * - `session-telemetry/record` — `dsh-session-telemetry` returns the record\n * straight out of the waterfall and hands it to the backend, so a Promise\n * would be emitted as a record: silent corruption, no error anywhere.\n * - `compaction/summary-error` — `dsh-compaction-basic` dispatches it as\n * `recover: (…) => this.ctx.waterfall(this, \"compaction/summary-error\", …, () => false)`\n * (no await) and consumes the boolean synchronously in\n * `if (!dependencies.recover(error, agent, prepared.shadowedSeqs, signal)) throw error`.\n * A Promise is always truthy, so `!recover(…)` is always false and every\n * summarizer failure is swallowed instead of rethrown — the compaction then\n * proceeds as if recovery had succeeded.\n *\n * Override per profile with `syncReturnHooks` once the host awaits one of them.\n */\nexport const DEFAULT_SYNC_RETURN_HOOKS: readonly string[] = [\n 'llm/stream',\n 'session-telemetry/record',\n 'compaction/summary-error',\n]\n\n/** The dsh serial hook controlled by default. */\nexport const DEFAULT_SERIAL_HOOKS: readonly string[] = ['agent/turn-stopping']\n\n/**\n * This plugin's Cordis Config schema.\n *\n * Since DSH 0.1.7 this *is* the Settings page: the fields are `.volatile()`, so\n * the settings service projects exactly them into an editable form and commits\n * an edit into the live reference rather than remounting the plugin. The\n * defaults are the real hook sets below, so an entry whose `config:` key is\n * empty resolves to them.\n */\nexport const Config = hooksOrderingConfig(DEFAULT_WATERFALL_HOOKS, DEFAULT_SERIAL_HOOKS)\n\n/** The same schema under its pre-0.1.7 name. */\nexport const HooksOrderingConfig = Config\n\n/**\n * Plugin config as a plain value.\n *\n * A loader-resolved config carries the volatile fields as references instead of\n * plain arrays, so this describes the *plain* shape callers and tests construct.\n */\nexport interface ConfigShape {\n /**\n * Waterfall hooks to control. Defaults to {@link DEFAULT_WATERFALL_HOOKS}.\n * Pass `[]` to disable the waterfall service entirely.\n */\n hooks?: readonly string[]\n /**\n * Serial hooks to control. Defaults to {@link DEFAULT_SERIAL_HOOKS}.\n * Pass `[]` to disable the serial service entirely.\n */\n serialHooks?: readonly string[]\n /**\n * Hooks whose return value the host consumes without awaiting, and which must\n * therefore refuse participants. Defaults to {@link DEFAULT_SYNC_RETURN_HOOKS}.\n * Pass `[]` — or a set without a given hook — to opt in to ordering it, once\n * the host awaits that dispatch.\n *\n * Composition-only: this describes the HOST's dispatch, not a user\n * preference, so it is intentionally not a field of {@link Config} and never\n * appears on the Settings page.\n */\n syncReturnHooks?: readonly string[]\n /** When set, the constraint DAG (JSON) is logged to this file on every change. */\n log?: string\n /** Overrides the loader entry id; only non-loader harnesses need it. */\n entryId?: string\n}\n\n/**\n * Mount {@link HookOrdering} and/or {@link SerialHookOrdering} and control the\n * configured hooks once the services are active.\n *\n * `config` may be `null`, and that is the ordinary case rather than an edge\n * case: a loader row whose `config:` key is followed only by comments parses as\n * YAML null, and a default parameter covers `undefined` but not `null`. This\n * package's own `cordis.patch.yml` is written exactly that way — every option\n * commented out — so a null config must mean \"all defaults\", not a crash.\n *\n * Since DSH 0.1.7 there is no second \"user layer\": the row's `config` IS the\n * editable settings, because Cordis validates it against {@link Config} and the\n * settings service projects that schema. A volatile field arrives here as a\n * reference rather than a plain array, which is why every read goes through\n * {@link readSetting} — that also keeps a plain Cordis mount (and an older\n * harness) working, where the fields are plain values.\n *\n * `hooks`/`serialHooks` are read once, at apply time. An edit from the Settings\n * page therefore needs a plugin reload to take effect — the same restart\n * semantics this namespace declared before 0.1.7 (`applies: 'restart'`), since\n * changing the controlled set means adding or removing live hook brackets and\n * the coordinator has no public \"release one hook\" operation.\n *\n * @param ctx - the Cordis context.\n * @param config - which hooks to control and an optional DAG `log` file; null means all defaults.\n */\nexport function apply(ctx: Context, config: ConfigShape | null = {}): void {\n const row = config ?? {}\n const settings: HooksOrderingSettings = {\n hooks: readSetting(row, 'hooks', DEFAULT_WATERFALL_HOOKS),\n serialHooks: readSetting(row, 'serialHooks', DEFAULT_SERIAL_HOOKS),\n log: readSetting(row, 'log', ''),\n }\n const { hooks, serialHooks, log } = settings\n const serviceConfig = log === '' ? {} : { log }\n // Not part of the settings form: the sync-return list describes the HOST's\n // dispatch, not a user preference, so it is composition (row) only.\n const syncReturnHooks = row.syncReturnHooks ?? DEFAULT_SYNC_RETURN_HOOKS\n\n const deps: string[] = []\n if (hooks.length > 0) {\n ctx.plugin(HookOrdering, { ...serviceConfig, syncReturnHooks })\n deps.push('hooksOrdering')\n }\n if (serialHooks.length > 0) {\n ctx.plugin(SerialHookOrdering, serviceConfig)\n deps.push('serialHooksOrdering')\n }\n if (deps.length === 0) return\n\n // The services activate asynchronously; control the hooks once they exist.\n ctx.inject(deps, (ready) => {\n for (const hook of hooks) ready.hooksOrdering.control(hook)\n for (const hook of serialHooks) ready.serialHooksOrdering.control(hook)\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA2DA,SAAgB,oBACd,uBACA,oBACQ;AACR,QAAO,OAAO,OAAO;EACnB,OAAO,OAAO,MAAM,OAAO,QAAQ,CAAC,CAAC,QAAQ,CAAC,GAAG,sBAAsB,CAAC,CAAC,UAAU;EACnF,aAAa,OAAO,MAAM,OAAO,QAAQ,CAAC,CAAC,QAAQ,CAAC,GAAG,mBAAmB,CAAC,CAAC,UAAU;EACtF,KAAK,OAAO,QAAQ,CAAC,QAAQ,GAAG,CAAC,UAAU;EAC5C,CAAC;;;;;;;;;;;;;;;AAgBJ,SAAgB,YACd,QACA,OACA,UACG;CACH,MAAM,QAAS,SAAwD;AACvE,KAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,KAAI,OAAQ,MAA4B,QAAQ,WAE9C,QADiB,MAA6B,KAAK,IAChC;AAErB,QAAO;;;;;;ACvET,MAAa,OAAO;;;;;;;;;;;;;;;;;;AAmBpB,MAAa,SAAS,CAAC,WAAW;;;;;;;;;;;;;;;;;;;AAoBlC,MAAaA,0BAA6C;CACxD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BD,MAAaC,4BAA+C;CAC1D;CACA;CACA;CACD;;AAGD,MAAaC,uBAA0C,CAAC,sBAAsB;;;;;;;;;;AAW9E,MAAa,SAAS,oBAAoB,yBAAyB,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiExF,SAAgB,MAAM,KAAc,SAA6B,EAAE,EAAQ;CACzE,MAAM,MAAM,UAAU,EAAE;CAMxB,MAAM,EAAE,OAAO,aAAa,QALY;EACtC,OAAO,YAAY,KAAK,SAAS,wBAAwB;EACzD,aAAa,YAAY,KAAK,eAAe,qBAAqB;EAClE,KAAK,YAAY,KAAK,OAAO,GAAG;EACjC;CAED,MAAM,gBAAgB,QAAQ,KAAK,EAAE,GAAG,EAAE,KAAK;CAG/C,MAAM,kBAAkB,IAAI,mBAAmB;CAE/C,MAAMC,OAAiB,EAAE;AACzB,KAAI,MAAM,SAAS,GAAG;AACpB,MAAI,OAAOC,mBAAc;GAAE,GAAG;GAAe;GAAiB,CAAC;AAC/D,OAAK,KAAK,gBAAgB;;AAE5B,KAAI,YAAY,SAAS,GAAG;AAC1B,MAAI,OAAO,oBAAoB,cAAc;AAC7C,OAAK,KAAK,sBAAsB;;AAElC,KAAI,KAAK,WAAW,EAAG;AAGvB,KAAI,OAAO,OAAO,UAAU;AAC1B,OAAK,MAAM,QAAQ,MAAO,OAAM,cAAc,QAAQ,KAAK;AAC3D,OAAK,MAAM,QAAQ,YAAa,OAAM,oBAAoB,QAAQ,KAAK;GACvE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zfdx123/dsh-hooks-ordering",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "钩子排序:为 Cordis 的 waterfall / serial 钩子提供确定性的 before/after 排序,让互相独立的插件能声明彼此的先后关系(拓扑排序、环检测、可配置控制集与设置页)。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cordis",
|
|
@@ -61,11 +61,11 @@
|
|
|
61
61
|
],
|
|
62
62
|
"engines": {
|
|
63
63
|
"node": "^22.19.0 || >=24.0.0",
|
|
64
|
-
"dsh": "^0.1.
|
|
64
|
+
"dsh": "^0.1.7-rc.2"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
68
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
67
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
68
|
+
"@deepseek-ai/dsh-settings": "^0.1.7-rc.2"
|
|
69
69
|
},
|
|
70
70
|
"peerDependenciesMeta": {
|
|
71
71
|
"@deepseek-ai/dsh-settings": {
|
|
@@ -73,10 +73,10 @@
|
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
76
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
79
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
80
80
|
"@types/node": "^22.20.1",
|
|
81
81
|
"@vitest/coverage-v8": "^3.2.4",
|
|
82
82
|
"oxlint": "^1.0.0",
|
package/src/dsh.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import type { Context } from '@deepseek-ai/cordis'
|
|
19
19
|
import HookOrdering from './waterfall.ts'
|
|
20
20
|
import { SerialHookOrdering } from './serial.ts'
|
|
21
|
-
import { type HooksOrderingSettings,
|
|
21
|
+
import { type HooksOrderingSettings, hooksOrderingConfig, readSetting } from './settings.ts'
|
|
22
22
|
|
|
23
23
|
/** Cordis plugin name used by loader diagnostics. */
|
|
24
24
|
export const name = 'hooks-ordering'
|
|
@@ -108,8 +108,27 @@ export const DEFAULT_SYNC_RETURN_HOOKS: readonly string[] = [
|
|
|
108
108
|
/** The dsh serial hook controlled by default. */
|
|
109
109
|
export const DEFAULT_SERIAL_HOOKS: readonly string[] = ['agent/turn-stopping']
|
|
110
110
|
|
|
111
|
-
/**
|
|
112
|
-
|
|
111
|
+
/**
|
|
112
|
+
* This plugin's Cordis Config schema.
|
|
113
|
+
*
|
|
114
|
+
* Since DSH 0.1.7 this *is* the Settings page: the fields are `.volatile()`, so
|
|
115
|
+
* the settings service projects exactly them into an editable form and commits
|
|
116
|
+
* an edit into the live reference rather than remounting the plugin. The
|
|
117
|
+
* defaults are the real hook sets below, so an entry whose `config:` key is
|
|
118
|
+
* empty resolves to them.
|
|
119
|
+
*/
|
|
120
|
+
export const Config = hooksOrderingConfig(DEFAULT_WATERFALL_HOOKS, DEFAULT_SERIAL_HOOKS)
|
|
121
|
+
|
|
122
|
+
/** The same schema under its pre-0.1.7 name. */
|
|
123
|
+
export const HooksOrderingConfig = Config
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Plugin config as a plain value.
|
|
127
|
+
*
|
|
128
|
+
* A loader-resolved config carries the volatile fields as references instead of
|
|
129
|
+
* plain arrays, so this describes the *plain* shape callers and tests construct.
|
|
130
|
+
*/
|
|
131
|
+
export interface ConfigShape {
|
|
113
132
|
/**
|
|
114
133
|
* Waterfall hooks to control. Defaults to {@link DEFAULT_WATERFALL_HOOKS}.
|
|
115
134
|
* Pass `[]` to disable the waterfall service entirely.
|
|
@@ -125,10 +144,16 @@ export interface Config {
|
|
|
125
144
|
* therefore refuse participants. Defaults to {@link DEFAULT_SYNC_RETURN_HOOKS}.
|
|
126
145
|
* Pass `[]` — or a set without a given hook — to opt in to ordering it, once
|
|
127
146
|
* the host awaits that dispatch.
|
|
147
|
+
*
|
|
148
|
+
* Composition-only: this describes the HOST's dispatch, not a user
|
|
149
|
+
* preference, so it is intentionally not a field of {@link Config} and never
|
|
150
|
+
* appears on the Settings page.
|
|
128
151
|
*/
|
|
129
152
|
syncReturnHooks?: readonly string[]
|
|
130
153
|
/** When set, the constraint DAG (JSON) is logged to this file on every change. */
|
|
131
154
|
log?: string
|
|
155
|
+
/** Overrides the loader entry id; only non-loader harnesses need it. */
|
|
156
|
+
entryId?: string
|
|
132
157
|
}
|
|
133
158
|
|
|
134
159
|
/**
|
|
@@ -141,25 +166,33 @@ export interface Config {
|
|
|
141
166
|
* package's own `cordis.patch.yml` is written exactly that way — every option
|
|
142
167
|
* commented out — so a null config must mean "all defaults", not a crash.
|
|
143
168
|
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
169
|
+
* Since DSH 0.1.7 there is no second "user layer": the row's `config` IS the
|
|
170
|
+
* editable settings, because Cordis validates it against {@link Config} and the
|
|
171
|
+
* settings service projects that schema. A volatile field arrives here as a
|
|
172
|
+
* reference rather than a plain array, which is why every read goes through
|
|
173
|
+
* {@link readSetting} — that also keeps a plain Cordis mount (and an older
|
|
174
|
+
* harness) working, where the fields are plain values.
|
|
175
|
+
*
|
|
176
|
+
* `hooks`/`serialHooks` are read once, at apply time. An edit from the Settings
|
|
177
|
+
* page therefore needs a plugin reload to take effect — the same restart
|
|
178
|
+
* semantics this namespace declared before 0.1.7 (`applies: 'restart'`), since
|
|
179
|
+
* changing the controlled set means adding or removing live hook brackets and
|
|
180
|
+
* the coordinator has no public "release one hook" operation.
|
|
148
181
|
*
|
|
149
182
|
* @param ctx - the Cordis context.
|
|
150
183
|
* @param config - which hooks to control and an optional DAG `log` file; null means all defaults.
|
|
151
184
|
*/
|
|
152
|
-
export function apply(ctx: Context, config:
|
|
185
|
+
export function apply(ctx: Context, config: ConfigShape | null = {}): void {
|
|
153
186
|
const row = config ?? {}
|
|
154
|
-
const
|
|
155
|
-
hooks: row
|
|
156
|
-
serialHooks: row
|
|
157
|
-
log: row
|
|
187
|
+
const settings: HooksOrderingSettings = {
|
|
188
|
+
hooks: readSetting(row, 'hooks', DEFAULT_WATERFALL_HOOKS),
|
|
189
|
+
serialHooks: readSetting(row, 'serialHooks', DEFAULT_SERIAL_HOOKS),
|
|
190
|
+
log: readSetting(row, 'log', ''),
|
|
158
191
|
}
|
|
159
|
-
const { hooks, serialHooks, log } =
|
|
192
|
+
const { hooks, serialHooks, log } = settings
|
|
160
193
|
const serviceConfig = log === '' ? {} : { log }
|
|
161
|
-
// Not part of the settings
|
|
162
|
-
//
|
|
194
|
+
// Not part of the settings form: the sync-return list describes the HOST's
|
|
195
|
+
// dispatch, not a user preference, so it is composition (row) only.
|
|
163
196
|
const syncReturnHooks = row.syncReturnHooks ?? DEFAULT_SYNC_RETURN_HOOKS
|
|
164
197
|
|
|
165
198
|
const deps: string[] = []
|
package/src/index.ts
CHANGED
|
@@ -45,7 +45,8 @@ export { type SerialHookEntry, type SerialHookOrderingConfig, SerialHookOrdering
|
|
|
45
45
|
// it as its default.
|
|
46
46
|
export {
|
|
47
47
|
apply,
|
|
48
|
-
|
|
48
|
+
Config,
|
|
49
|
+
type ConfigShape,
|
|
49
50
|
DEFAULT_SERIAL_HOOKS,
|
|
50
51
|
DEFAULT_SYNC_RETURN_HOOKS,
|
|
51
52
|
DEFAULT_WATERFALL_HOOKS,
|
package/src/settings.ts
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* from the Settings page instead of a `cordis.patch.yml` row.
|
|
2
|
+
* This plugin's Cordis Config — and, since DSH 0.1.7, its Settings page.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
4
|
+
* 0.1.7 removed `ctx.settings.register(ns, schema)`. Settings are now *projected
|
|
5
|
+
* from each entry's own Config*: the loader validates the row's `config` against
|
|
6
|
+
* this schema, resolves the defaults, and the settings service renders exactly
|
|
7
|
+
* the nodes marked `.volatile()` as an editable form. Three consequences are
|
|
8
|
+
* load-bearing here:
|
|
6
9
|
*
|
|
7
|
-
* - The
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* - The defaults below are the *real* defaults, not "off" placeholders. Nothing
|
|
11
|
+
* supplies a second `base` layer any more, so an entry whose `config:` key is
|
|
12
|
+
* empty must resolve to the hook set this plugin controls by default.
|
|
13
|
+
* - `.volatile()` belongs on the individual fields, never on the root object:
|
|
14
|
+
* a volatile root collapses the whole Config into a single reference and
|
|
15
|
+
* discards every nested default (verified against schemastery 3.18.4), and
|
|
16
|
+
* schemastery rejects a volatile node nested inside another volatile node.
|
|
17
|
+
* - The schema is built lazily by {@link hooksOrderingConfig}. `dsh.ts` imports
|
|
18
|
+
* this module and this module needs `dsh.ts`'s default hook lists, so building
|
|
19
|
+
* the schema eagerly would read those bindings before they are initialized.
|
|
20
|
+
*
|
|
21
|
+
* `syncReturnHooks` is deliberately absent: it describes the HOST's dispatch
|
|
22
|
+
* (which hooks are consumed without awaiting), not a user preference, so it
|
|
23
|
+
* stays composition-only in `apply()`.
|
|
14
24
|
*
|
|
15
25
|
* @module dsh-hooks-ordering/settings
|
|
16
26
|
*/
|
|
17
27
|
|
|
18
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
19
28
|
import Schema from '@deepseek-ai/schemastery'
|
|
20
29
|
|
|
21
|
-
/** The
|
|
30
|
+
/** The fields this plugin exposes as editable settings. */
|
|
22
31
|
export interface HooksOrderingSettings {
|
|
23
32
|
/** Waterfall hooks to control; `[]` disables the waterfall service entirely. */
|
|
24
33
|
readonly hooks: readonly string[]
|
|
@@ -28,70 +37,60 @@ export interface HooksOrderingSettings {
|
|
|
28
37
|
readonly log: string
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
/** Namespace name — dsh requires a lowercase hyphenated identifier. */
|
|
32
|
-
export const SETTINGS_NS = 'hooks-ordering'
|
|
33
|
-
|
|
34
40
|
/**
|
|
35
|
-
*
|
|
41
|
+
* Entry id used by `cordis.patch.yml` and, in 0.1.7, by the settings form.
|
|
36
42
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* caller as the composition `base` layer (see {@link resolveSettings}), so a
|
|
40
|
-
* namespace the user has never touched still resolves to a complete object.
|
|
43
|
+
* The form is keyed by the **loader entry id**; this constant is the shipped
|
|
44
|
+
* row's id, and it is also the client half's settings-section id.
|
|
41
45
|
*/
|
|
42
|
-
export const
|
|
43
|
-
hooks: Schema.array(Schema.string()).default([]),
|
|
44
|
-
serialHooks: Schema.array(Schema.string()).default([]),
|
|
45
|
-
log: Schema.string().default(''),
|
|
46
|
-
})
|
|
46
|
+
export const SETTINGS_NS = 'hooks-ordering'
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
49
|
+
* Build the Config schema from the caller's default hook lists.
|
|
50
|
+
*
|
|
51
|
+
* Every field is `.volatile()`, so every field is user-editable from the
|
|
52
|
+
* Settings page and an edit is committed into the live reference instead of
|
|
53
|
+
* remounting the plugin. `log` is volatile too, because a non-volatile field
|
|
54
|
+
* would not appear in the form at all.
|
|
50
55
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
56
|
+
* @param defaultWaterfallHooks - waterfall hooks controlled when unconfigured.
|
|
57
|
+
* @param defaultSerialHooks - serial hooks controlled when unconfigured.
|
|
58
|
+
* @returns a callable schemastery schema, which is what Cordis resolves through.
|
|
54
59
|
*/
|
|
55
|
-
export
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
ns: string,
|
|
65
|
-
schema: unknown,
|
|
66
|
-
options?: { readonly base?: unknown; readonly applies?: 'live' | 'restart' },
|
|
67
|
-
): { get(): HooksOrderingSettings }
|
|
60
|
+
export function hooksOrderingConfig(
|
|
61
|
+
defaultWaterfallHooks: readonly string[],
|
|
62
|
+
defaultSerialHooks: readonly string[],
|
|
63
|
+
): Schema {
|
|
64
|
+
return Schema.object({
|
|
65
|
+
hooks: Schema.array(Schema.string()).default([...defaultWaterfallHooks]).volatile(),
|
|
66
|
+
serialHooks: Schema.array(Schema.string()).default([...defaultSerialHooks]).volatile(),
|
|
67
|
+
log: Schema.string().default('').volatile(),
|
|
68
|
+
})
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* `base` is the composition layer — what the row (or the built-in defaults)
|
|
74
|
-
* asks for — so the user layer edits *over* it and a reset returns to it rather
|
|
75
|
-
* than to an empty form.
|
|
72
|
+
* Read one resolved field, unwrapping a volatile reference when the loader
|
|
73
|
+
* supplied one and falling back to a default when the field is absent.
|
|
76
74
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* cleanly instead of re-wiring the dispatch chain mid-flight.
|
|
75
|
+
* A loader-provided Config carries volatile fields as references (`.get()`);
|
|
76
|
+
* a plain Cordis mount, or an older harness, passes plain values. Both must
|
|
77
|
+
* work, and an absent field must not be confused with an explicit empty one.
|
|
81
78
|
*
|
|
82
|
-
* @param
|
|
83
|
-
* @param
|
|
84
|
-
* @
|
|
85
|
-
*
|
|
86
|
-
* registration failed — the caller then uses `base`.
|
|
79
|
+
* @param config - the resolved plugin config (possibly `null`).
|
|
80
|
+
* @param field - the field to read.
|
|
81
|
+
* @param fallback - value to use when the field is absent.
|
|
82
|
+
* @returns the field's current value, or `fallback`.
|
|
87
83
|
*/
|
|
88
|
-
export function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
84
|
+
export function readSetting<T>(
|
|
85
|
+
config: object | null | undefined,
|
|
86
|
+
field: string,
|
|
87
|
+
fallback: T,
|
|
88
|
+
): T {
|
|
89
|
+
const value = (config as Record<string, unknown> | null | undefined)?.[field]
|
|
90
|
+
if (value === undefined || value === null) return fallback
|
|
91
|
+
if (typeof (value as { get?: unknown }).get === 'function') {
|
|
92
|
+
const current = (value as { get(): unknown }).get()
|
|
93
|
+
return (current ?? fallback) as T
|
|
96
94
|
}
|
|
95
|
+
return value as T
|
|
97
96
|
}
|