dsh-advisor 0.2.2 → 0.2.4
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.i18n.yaml +2 -2
- package/README.md +2 -1
- package/README.zh.md +2 -1
- package/lib/client/advisor-card.d.ts +8 -4
- package/lib/client/advisor-store.d.ts +13 -3
- package/lib/client/index.d.ts +5 -5
- package/lib/client.js +33 -31
- package/package.json +17 -19
- package/scripts/build-client.mjs +0 -2
- package/scripts/prepare-release.mjs +2 -2
package/README.i18n.yaml
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
# editing either side, bring the other along and re-record with:
|
|
4
4
|
# git hash-object README.md
|
|
5
5
|
# git hash-object README.zh.md
|
|
6
|
-
README.md:
|
|
7
|
-
README.zh.md:
|
|
6
|
+
README.md: 5caf5c759a30682228bc562d914d801b64f94f08
|
|
7
|
+
README.zh.md: 7913542a4f734da470b0f334a8a2870c9a2c06a3
|
package/README.md
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|

|
|
7
|
-

|
|
8
|
+

|
|
8
9
|
[](https://dshfind.com/plugins/omdsh-dev/dsh-advisor?ref=badge)
|
|
9
10
|
|
|
10
11
|
A standalone dsh (DeepSeek Harness) plugin bundle porting the omp "advisor" subsystem: a per-session independent reviewer model that observes the primary transcript, reviews each stepped turn with an explicitly configured model (provider + model are required), and injects severity-ranked advice (nit / concern / blocker) back into the session — without polluting or recursively reviewing itself.
|
package/README.zh.md
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|

|
|
7
|
-

|
|
8
|
+

|
|
8
9
|
[](https://dshfind.com/zh/plugins/omdsh-dev/dsh-advisor?ref=badge)
|
|
9
10
|
|
|
10
11
|
一个移植 omp「advisor」子系统的独立 dsh(DeepSeek Harness)插件组合包:一个按会话运行的独立评审模型,观察主会话 transcript,用显式配置的模型(provider 与 model 均为必填)评审每个已完成的 stepped turn,并把按严重度排序的建议(nit / concern / blocker)注入回会话——不污染主循环,也不递归地评审自己。
|
|
@@ -63,14 +63,18 @@
|
|
|
63
63
|
*/
|
|
64
64
|
import { type ReactNode } from 'react';
|
|
65
65
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
66
|
-
import type {
|
|
66
|
+
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
|
|
67
67
|
import type { AdvisorSettingsState, AdvisorSettingsStore } from './advisor-store.ts';
|
|
68
|
-
/** Injected dependencies of {@link AdvisorCard} (slot `inject`).
|
|
68
|
+
/** Injected dependencies of {@link AdvisorCard} (slot `inject`). The `hooks`
|
|
69
|
+
* compartment carries the bare snapshot store; the slot renderer binds it to
|
|
70
|
+
* the `useSnapshot` selector hook the component consumes (host renderer contract). */
|
|
69
71
|
export interface AdvisorCardInjected {
|
|
70
72
|
/** The card store (loaded on mount, refreshed on pushed invalidations). */
|
|
71
73
|
controller: AdvisorSettingsStore;
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
+
/** Bare snapshot source the renderer binds as `useSnapshot`. */
|
|
75
|
+
hooks: {
|
|
76
|
+
snapshot: SnapshotStore<AdvisorSettingsState>;
|
|
77
|
+
};
|
|
74
78
|
}
|
|
75
79
|
/**
|
|
76
80
|
* Props the renderer binds for the card: the `settings.plugin.item` runtime
|
|
@@ -39,6 +39,13 @@
|
|
|
39
39
|
*/
|
|
40
40
|
import type { ClientConnectionRpc, IApiClient, SettingsNamespaceView } from '@deepseek-ai/dsh-client-connection/client';
|
|
41
41
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
|
|
42
|
+
import type { SettingsSchemaService } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
43
|
+
/**
|
|
44
|
+
* The schema face the card store uses: ui-settings owns the immutable
|
|
45
|
+
* path writers (`ctx.settingsSchema`); this narrow pick hides the Cordis
|
|
46
|
+
* service identity from the store and its tests.
|
|
47
|
+
*/
|
|
48
|
+
export type SettingsSchemaOperations = Pick<SettingsSchemaService, 'getPath' | 'setPath' | 'deletePath'>;
|
|
42
49
|
/**
|
|
43
50
|
* The wire `config` value the host gateway returns — mirror of the node-side
|
|
44
51
|
* `ResolvedAdvisorConfig` (src/config.ts). Kept as a local structural type
|
|
@@ -177,6 +184,7 @@ export interface AdvisorSettingsState {
|
|
|
177
184
|
export declare class AdvisorSettingsStore {
|
|
178
185
|
private readonly api;
|
|
179
186
|
private readonly rpc;
|
|
187
|
+
private readonly schema;
|
|
180
188
|
/** The snapshot the card renders from (uSES-safe store). */
|
|
181
189
|
readonly store: SnapshotStore<AdvisorSettingsState>;
|
|
182
190
|
/** Latest load wins; an older response never overwrites a newer one. */
|
|
@@ -203,7 +211,9 @@ export declare class AdvisorSettingsStore {
|
|
|
203
211
|
* @param rpc - the connection's generic RPC caller for the host gateway
|
|
204
212
|
* channel (`/api`), injected from the connection handle.
|
|
205
213
|
*/
|
|
206
|
-
constructor(api: Pick<IApiClient, 'settings' | 'llm'>, rpc: ClientConnectionRpc);
|
|
214
|
+
constructor(api: Pick<IApiClient, 'settings' | 'llm'>, rpc: ClientConnectionRpc, schema: SettingsSchemaOperations);
|
|
215
|
+
/** Profile-declared models (the provider's `models` directory), when the profile has one. */
|
|
216
|
+
private profileModels;
|
|
207
217
|
/**
|
|
208
218
|
* Refresh the whole page snapshot: the provider directory and the settings
|
|
209
219
|
* namespaces in parallel, then the advisor config over the gateway channel,
|
|
@@ -296,9 +306,9 @@ export declare class AdvisorSettingsStore {
|
|
|
296
306
|
* write (the frozen-snapshot write would be dropped by the snapshot store).
|
|
297
307
|
*/
|
|
298
308
|
private recomputeDirty;
|
|
299
|
-
/** Edit one always-present draft field via the schema
|
|
309
|
+
/** Edit one always-present draft field via the settings schema path writer. */
|
|
300
310
|
private setField;
|
|
301
|
-
/** Remove one optional draft field via the schema
|
|
311
|
+
/** Remove one optional draft field via the settings schema path writer. */
|
|
302
312
|
private clearField;
|
|
303
313
|
private clampInt;
|
|
304
314
|
}
|
package/lib/client/index.d.ts
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* provider directory through the connection wire, and keeps fresh on pushed
|
|
8
8
|
* invalidations. Export discipline: the client half value-imports ONLY the
|
|
9
9
|
* frozen platform module table (CLIENT_EXTERNALS: react /
|
|
10
|
-
* @deepseek-ai/cordis / ui-slots /
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* @deepseek-ai/cordis / ui-slots / ui-primitives / the documented
|
|
11
|
+
* `@deepseek-ai/dsh-client-runtime/client` exemption); every other
|
|
12
|
+
* `@deepseek-ai/*` import is type-only (erased at build) — values arrive via
|
|
13
|
+
* cordis injection (`ctx.get('connection')`, slot inject faces, the
|
|
14
|
+
* `settingsSchema` service). Mirrors the ui-models reference entry.
|
|
15
15
|
*/
|
|
16
16
|
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
17
17
|
import { type AdvisorKey } from './locales.ts';
|
package/lib/client.js
CHANGED
|
@@ -29,7 +29,6 @@ __export(index_exports, {
|
|
|
29
29
|
refreshIfLoaded: () => refreshIfLoaded
|
|
30
30
|
});
|
|
31
31
|
module.exports = __toCommonJS(index_exports);
|
|
32
|
-
var import_dsh_client_web_react = require("@deepseek-ai/dsh-client-web-react");
|
|
33
32
|
|
|
34
33
|
// src/client/advisor-card.tsx
|
|
35
34
|
var import_react = require("react");
|
|
@@ -285,7 +284,6 @@ function AdvisorCard(props) {
|
|
|
285
284
|
|
|
286
285
|
// src/client/advisor-store.ts
|
|
287
286
|
var import_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
288
|
-
var import_dsh_client_schema_form = require("@deepseek-ai/dsh-client-schema-form");
|
|
289
287
|
function defaultDraft() {
|
|
290
288
|
return { enabled: false, systemPrompt: "", immuneTurns: 3, maxDeltaMessages: 60 };
|
|
291
289
|
}
|
|
@@ -311,20 +309,6 @@ function gateFailure(draft) {
|
|
|
311
309
|
if (draft.model === void 0) return "model";
|
|
312
310
|
return void 0;
|
|
313
311
|
}
|
|
314
|
-
function profileModels(profile) {
|
|
315
|
-
const models = (0, import_dsh_client_schema_form.getPath)(profile, ["models"]);
|
|
316
|
-
if (!Array.isArray(models)) return void 0;
|
|
317
|
-
return models.flatMap((entry) => {
|
|
318
|
-
if (typeof entry !== "object" || entry === null) return [];
|
|
319
|
-
const model = entry;
|
|
320
|
-
if (typeof model.id !== "string" || model.id.length === 0) return [];
|
|
321
|
-
return [{
|
|
322
|
-
id: model.id,
|
|
323
|
-
name: typeof model.name === "string" ? model.name : model.id,
|
|
324
|
-
...typeof model.description === "string" ? { description: model.description } : {}
|
|
325
|
-
}];
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
312
|
var AdvisorSettingsStore = class {
|
|
329
313
|
/**
|
|
330
314
|
* @param api - the wire face (settings/llm domains) for the provider/model
|
|
@@ -332,9 +316,10 @@ var AdvisorSettingsStore = class {
|
|
|
332
316
|
* @param rpc - the connection's generic RPC caller for the host gateway
|
|
333
317
|
* channel (`/api`), injected from the connection handle.
|
|
334
318
|
*/
|
|
335
|
-
constructor(api, rpc) {
|
|
319
|
+
constructor(api, rpc, schema) {
|
|
336
320
|
__publicField(this, "api", api);
|
|
337
321
|
__publicField(this, "rpc", rpc);
|
|
322
|
+
__publicField(this, "schema", schema);
|
|
338
323
|
/** The snapshot the card renders from (uSES-safe store). */
|
|
339
324
|
__publicField(this, "store", (0, import_client.createSnapshotStore)({
|
|
340
325
|
status: "idle",
|
|
@@ -375,6 +360,21 @@ var AdvisorSettingsStore = class {
|
|
|
375
360
|
/** The resolved advisor config at the last get/apply (patch diff baseline). */
|
|
376
361
|
__publicField(this, "seed", defaultDraft());
|
|
377
362
|
}
|
|
363
|
+
/** Profile-declared models (the provider's `models` directory), when the profile has one. */
|
|
364
|
+
profileModels(profile) {
|
|
365
|
+
const models = this.schema.getPath(profile, ["models"]);
|
|
366
|
+
if (!Array.isArray(models)) return void 0;
|
|
367
|
+
return models.flatMap((entry) => {
|
|
368
|
+
if (typeof entry !== "object" || entry === null) return [];
|
|
369
|
+
const model = entry;
|
|
370
|
+
if (typeof model.id !== "string" || model.id.length === 0) return [];
|
|
371
|
+
return [{
|
|
372
|
+
id: model.id,
|
|
373
|
+
name: typeof model.name === "string" ? model.name : model.id,
|
|
374
|
+
...typeof model.description === "string" ? { description: model.description } : {}
|
|
375
|
+
}];
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
378
|
/**
|
|
379
379
|
* Refresh the whole page snapshot: the provider directory and the settings
|
|
380
380
|
* namespaces in parallel, then the advisor config over the gateway channel,
|
|
@@ -429,7 +429,7 @@ var AdvisorSettingsStore = class {
|
|
|
429
429
|
const options = [];
|
|
430
430
|
for (const entry of providers) {
|
|
431
431
|
const namespace = namespaces[entry.settingsNs];
|
|
432
|
-
const configured = namespace !== void 0 && (entry.settingsPath.length === 0 ||
|
|
432
|
+
const configured = namespace !== void 0 && (entry.settingsPath.length === 0 || this.schema.getPath(namespace.value, entry.settingsPath) !== void 0);
|
|
433
433
|
if (!configured) continue;
|
|
434
434
|
options.push({
|
|
435
435
|
provider: entry.provider,
|
|
@@ -483,8 +483,8 @@ var AdvisorSettingsStore = class {
|
|
|
483
483
|
const option = state.providers.find((candidate) => candidate.provider === provider);
|
|
484
484
|
if (option === void 0) return;
|
|
485
485
|
const namespace = state.namespaces[option.settingsNs];
|
|
486
|
-
const profile = namespace !== void 0 ?
|
|
487
|
-
const declared = profileModels(profile);
|
|
486
|
+
const profile = namespace !== void 0 ? this.schema.getPath(namespace.value, option.settingsPath) : void 0;
|
|
487
|
+
const declared = this.profileModels(profile);
|
|
488
488
|
if (declared !== void 0) {
|
|
489
489
|
this.store.update((s) => {
|
|
490
490
|
if (declared.length > 0) {
|
|
@@ -553,8 +553,8 @@ var AdvisorSettingsStore = class {
|
|
|
553
553
|
const trimmed = provider.trim();
|
|
554
554
|
this.store.update((s) => {
|
|
555
555
|
const draft = s.draft;
|
|
556
|
-
const withoutProvider = trimmed.length === 0 ?
|
|
557
|
-
s.draft =
|
|
556
|
+
const withoutProvider = trimmed.length === 0 ? this.schema.deletePath(draft, ["provider"]) : this.schema.setPath(draft, ["provider"], trimmed);
|
|
557
|
+
s.draft = this.schema.deletePath(withoutProvider, ["model"]);
|
|
558
558
|
s.applyState = { kind: "idle" };
|
|
559
559
|
s.dirty = this.recomputeDirty(s.draft);
|
|
560
560
|
});
|
|
@@ -716,18 +716,18 @@ var AdvisorSettingsStore = class {
|
|
|
716
716
|
recomputeDirty(draft) {
|
|
717
717
|
return Object.keys(this.patchFor(draft)).length > 0;
|
|
718
718
|
}
|
|
719
|
-
/** Edit one always-present draft field via the schema
|
|
719
|
+
/** Edit one always-present draft field via the settings schema path writer. */
|
|
720
720
|
setField(key, value) {
|
|
721
721
|
this.store.update((s) => {
|
|
722
|
-
s.draft =
|
|
722
|
+
s.draft = this.schema.setPath(s.draft, [key], value);
|
|
723
723
|
s.applyState = { kind: "idle" };
|
|
724
724
|
s.dirty = this.recomputeDirty(s.draft);
|
|
725
725
|
});
|
|
726
726
|
}
|
|
727
|
-
/** Remove one optional draft field via the schema
|
|
727
|
+
/** Remove one optional draft field via the settings schema path writer. */
|
|
728
728
|
clearField(key) {
|
|
729
729
|
this.store.update((s) => {
|
|
730
|
-
s.draft =
|
|
730
|
+
s.draft = this.schema.deletePath(s.draft, [key]);
|
|
731
731
|
s.applyState = { kind: "idle" };
|
|
732
732
|
s.dirty = this.recomputeDirty(s.draft);
|
|
733
733
|
});
|
|
@@ -805,12 +805,11 @@ var zh = {
|
|
|
805
805
|
|
|
806
806
|
// src/client/index.ts
|
|
807
807
|
var NS = "settings.advisor";
|
|
808
|
-
var inject = ["slots", "locale", "connection"];
|
|
808
|
+
var inject = ["slots", "locale", "connection", "settingsSchema"];
|
|
809
809
|
function apply(ctx) {
|
|
810
810
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "advisor: copy dictionaries");
|
|
811
811
|
const connection = ctx.get("connection");
|
|
812
|
-
const controller = new AdvisorSettingsStore(connection.api, connection.rpc);
|
|
813
|
-
const useSnapshot = (0, import_dsh_client_web_react.bindSnapshotSelector)(controller.store);
|
|
812
|
+
const controller = new AdvisorSettingsStore(connection.api, connection.rpc, ctx.settingsSchema);
|
|
814
813
|
ctx.effect(() => {
|
|
815
814
|
let pending = false;
|
|
816
815
|
const refresh = () => {
|
|
@@ -834,7 +833,7 @@ function apply(ctx) {
|
|
|
834
833
|
ctx.slots.inject("settings.plugin.item", function* () {
|
|
835
834
|
yield ctx.slots.register({
|
|
836
835
|
name: "settings.plugin.item",
|
|
837
|
-
//
|
|
836
|
+
// Keyed slot: the key is the settings namespace the card edits —
|
|
838
837
|
// the already-installed `advisor` namespace (ADVISOR_SETTINGS_NAMESPACE
|
|
839
838
|
// in src/settings.ts; spelled as a literal here because the host-half
|
|
840
839
|
// settings module is outside the client bundle's externals). Keyed
|
|
@@ -842,7 +841,10 @@ function apply(ctx) {
|
|
|
842
841
|
// order, which still places this card after the upstream three.
|
|
843
842
|
key: "advisor",
|
|
844
843
|
locale: NS,
|
|
845
|
-
|
|
844
|
+
// Hooks compartment: the renderer binds `hooks.snapshot` to the
|
|
845
|
+
// component's `useSnapshot` selector hook (the old web-react
|
|
846
|
+
// bindSnapshotSelector call is gone with that package).
|
|
847
|
+
inject: () => ({ controller, hooks: { snapshot: controller.store } })
|
|
846
848
|
}, AdvisorCard);
|
|
847
849
|
});
|
|
848
850
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-advisor",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "dsh plugin bundle porting the omp advisor subsystem: a per-session reviewer model that observes the primary transcript and injects severity-ranked advice (nit/concern/blocker).",
|
|
6
6
|
"repository": {
|
|
@@ -51,24 +51,22 @@
|
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
54
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
55
|
-
"@deepseek-ai/dsh-api-gateway": "^0.1.
|
|
56
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.
|
|
57
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.
|
|
58
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.
|
|
59
|
-
"@deepseek-ai/dsh-client-
|
|
60
|
-
"@deepseek-ai/dsh-client-ui-
|
|
61
|
-
"@deepseek-ai/dsh-client-ui-
|
|
62
|
-
"@deepseek-ai/dsh-client-ui-
|
|
63
|
-
"@deepseek-ai/dsh-
|
|
64
|
-
"@deepseek-ai/dsh-
|
|
65
|
-
"@deepseek-ai/dsh-
|
|
66
|
-
"@deepseek-ai/dsh-
|
|
67
|
-
"@deepseek-ai/dsh-
|
|
68
|
-
"@deepseek-ai/dsh-
|
|
69
|
-
"@deepseek-ai/dsh-
|
|
70
|
-
"@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.7",
|
|
71
|
-
"@deepseek-ai/dsh-typert-registry": "^0.1.0-rc.7",
|
|
54
|
+
"@deepseek-ai/dsh-agent": "^0.1.1-rc.1",
|
|
55
|
+
"@deepseek-ai/dsh-api-gateway": "^0.1.1-rc.1",
|
|
56
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.1-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.1-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.1",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.1-rc.1",
|
|
60
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.1",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.1-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.1",
|
|
63
|
+
"@deepseek-ai/dsh-commands": "^0.1.1-rc.1",
|
|
64
|
+
"@deepseek-ai/dsh-llm": "^0.1.1-rc.1",
|
|
65
|
+
"@deepseek-ai/dsh-session": "^0.1.1-rc.1",
|
|
66
|
+
"@deepseek-ai/dsh-settings": "^0.1.1-rc.1",
|
|
67
|
+
"@deepseek-ai/dsh-timeout": "^0.1.1-rc.1",
|
|
68
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-typert-registry": "^0.1.1-rc.1",
|
|
72
70
|
"react": "^18.2.0",
|
|
73
71
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
74
72
|
},
|
package/scripts/build-client.mjs
CHANGED
|
@@ -53,9 +53,7 @@ export const CLIENT_EXTERNALS = [
|
|
|
53
53
|
'react-dom/client',
|
|
54
54
|
'@deepseek-ai/cordis',
|
|
55
55
|
'@deepseek-ai/dsh-client-ui-slots',
|
|
56
|
-
'@deepseek-ai/dsh-client-web-react',
|
|
57
56
|
'@deepseek-ai/dsh-client-ui-primitives',
|
|
58
|
-
'@deepseek-ai/dsh-client-schema-form',
|
|
59
57
|
'@deepseek-ai/dsh-client-runtime/client',
|
|
60
58
|
]
|
|
61
59
|
|
|
@@ -37,8 +37,8 @@ const CHANGELOG_HEADER =
|
|
|
37
37
|
* suffix); null when not parseable. The suffix character set matches the
|
|
38
38
|
* workflows' guard regex `[0-9A-Za-z.-]+` (keep-simple: no leading-zero or
|
|
39
39
|
* empty-identifier rules), with one tightening: the suffix MUST contain at
|
|
40
|
-
* least one digit, so `
|
|
41
|
-
* (no numeric identifier) and `
|
|
40
|
+
* least one digit, so `1.2.3-alpha.1` / `1.2.3-rc.2` parse but `1.2.3-alpha`
|
|
41
|
+
* (no numeric identifier) and `1.2.3-` (empty suffix) are rejected. Returns
|
|
42
42
|
* { major, minor, patch, prerelease? } — prerelease is the raw suffix text
|
|
43
43
|
* (e.g. 'alpha.1') when present, undefined for formal versions.
|
|
44
44
|
*/
|