@dsh-triage/core 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -2
  3. package/dist/admission.d.ts +65 -0
  4. package/dist/admission.d.ts.map +1 -0
  5. package/dist/admission.js +153 -0
  6. package/dist/admission.js.map +1 -0
  7. package/dist/capabilities.d.ts +69 -0
  8. package/dist/capabilities.d.ts.map +1 -0
  9. package/dist/capabilities.js +93 -0
  10. package/dist/capabilities.js.map +1 -0
  11. package/dist/compat-channel.d.ts +60 -0
  12. package/dist/compat-channel.d.ts.map +1 -0
  13. package/dist/compat-channel.js +117 -0
  14. package/dist/compat-channel.js.map +1 -0
  15. package/dist/duplicates.d.ts +50 -0
  16. package/dist/duplicates.d.ts.map +1 -0
  17. package/dist/duplicates.js +103 -0
  18. package/dist/duplicates.js.map +1 -0
  19. package/dist/env.d.ts +45 -0
  20. package/dist/env.d.ts.map +1 -0
  21. package/dist/env.js +149 -0
  22. package/dist/env.js.map +1 -0
  23. package/dist/index.d.ts +38 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +23 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/inventory.d.ts +193 -0
  28. package/dist/inventory.d.ts.map +1 -0
  29. package/dist/inventory.js +762 -0
  30. package/dist/inventory.js.map +1 -0
  31. package/dist/ledger.d.ts +96 -0
  32. package/dist/ledger.d.ts.map +1 -0
  33. package/dist/ledger.js +218 -0
  34. package/dist/ledger.js.map +1 -0
  35. package/dist/messages.d.ts +17 -0
  36. package/dist/messages.d.ts.map +1 -0
  37. package/dist/messages.js +260 -0
  38. package/dist/messages.js.map +1 -0
  39. package/dist/outcome.d.ts +143 -0
  40. package/dist/outcome.d.ts.map +1 -0
  41. package/dist/outcome.js +376 -0
  42. package/dist/outcome.js.map +1 -0
  43. package/dist/redact.d.ts +30 -0
  44. package/dist/redact.d.ts.map +1 -0
  45. package/dist/redact.js +62 -0
  46. package/dist/redact.js.map +1 -0
  47. package/dist/result.d.ts +27 -0
  48. package/dist/result.d.ts.map +1 -0
  49. package/dist/result.js +15 -0
  50. package/dist/result.js.map +1 -0
  51. package/dist/types.d.ts +100 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +13 -0
  54. package/dist/types.js.map +1 -0
  55. package/dist/upgrade.d.ts +202 -0
  56. package/dist/upgrade.d.ts.map +1 -0
  57. package/dist/upgrade.js +538 -0
  58. package/dist/upgrade.js.map +1 -0
  59. package/dist/verdict.d.ts +101 -0
  60. package/dist/verdict.d.ts.map +1 -0
  61. package/dist/verdict.js +759 -0
  62. package/dist/verdict.js.map +1 -0
  63. package/dist/version-drift.d.ts +82 -0
  64. package/dist/version-drift.d.ts.map +1 -0
  65. package/dist/version-drift.js +133 -0
  66. package/dist/version-drift.js.map +1 -0
  67. package/package.json +31 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DSH-Addition contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,30 @@
1
- # Temporary Holding Version
1
+ # @dsh-triage/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Pure, offline judgment engine for DeepSeek Harness plugin compatibility.
4
+
5
+ ## Public API
6
+
7
+ | Export | Purpose |
8
+ |---|---|
9
+ | `evaluateAdmission(manifest, runtime)` | Mirror upstream admission semantics (`engines.dsh` wins, else `@deepseek-ai/dsh*` peers ANDed, prereleases participate, empty/invalid ranges incompatible) |
10
+ | `evaluateRange(range, runtime)` | Single-range evaluation that never throws |
11
+ | `loadEnv({ dshHome, profile })` | Read a profile fingerprint (`Result`, no throw) |
12
+ | `parseProfileManifest(raw)` | Pure manifest projection |
13
+ | `parsePatchYaml(text)` / `extractEntryClaims(doc, source)` | Loader row extraction from a patch layer |
14
+ | `findEntryIdCollisions(claims)` | Duplicate loader entry ids (fatal at boot) |
15
+ | `verdictFromAdmission(input)` / `verdictsFromCollisions(input)` | Verdict assembly |
16
+ | `mergeVerdicts(verdicts)` | Worst-conclusion merge with dedupe |
17
+ | `renderVerdict(verdict, locale)` | Deterministic text rendering (`en` / `zh`) |
18
+
19
+ ## Invariants
20
+
21
+ - Only `env.ts` touches the filesystem; everything else is pure.
22
+ - No clock, no randomness: timestamps are injected by the caller.
23
+ - Conclusions are exactly `go | risky | blocked | unknown`.
24
+ - `blocked`/`risky` always carry remediation; `unknown` always lists missing evidence.
25
+
26
+ ## Parity
27
+
28
+ Behaviour is locked by `data/gold/admission-cases.json` (13 synthetic + 8 real npm snapshots) and enforced by `scripts/gate_admission_parity.mjs`. Change the fixtures and the spec before changing the rules.
29
+
30
+ MIT.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Mirrors DeepSeek Harness admission semantics.
3
+ *
4
+ * SOURCE OF TRUTH (do not "improve" these rules from memory):
5
+ * `@deepseek-ai/dsh-app-boot@0.2.1-rc.2/README.md` — "Before a profile imports a
6
+ * plugin, DSH checks its declared DSH requirement against the single runtime
7
+ * version returned by `getDshRuntimeVersion()`":
8
+ *
9
+ * 1. `engines.dsh` present -> that range alone decides, peers are ignored.
10
+ * 2. otherwise -> every peer range on `@deepseek-ai/dsh` and
11
+ * `@deepseek-ai/dsh-*` must match (AND).
12
+ * 3. prereleases participate in range matching.
13
+ * 4. empty / invalid ranges (and `workspace:` in `engines.dsh`) are incompatible.
14
+ * 5. no declaration at all -> no constraint.
15
+ *
16
+ * Parity is locked by `data/gold/admission-cases.json` and enforced by
17
+ * `scripts/gate_admission_parity.mjs`. Change the rules there first.
18
+ */
19
+ /** Why a declared range did not hold. Stable codes, safe to switch on. */
20
+ export type RequirementFailure = 'empty-range' | 'invalid-range' | 'workspace-protocol' | 'unsatisfied' | 'malformed-manifest';
21
+ export interface Requirement {
22
+ /** `dsh` for `engines.dsh`, otherwise the peer package name. */
23
+ readonly key: string;
24
+ readonly range: string;
25
+ readonly satisfied: boolean;
26
+ readonly failure: RequirementFailure | null;
27
+ }
28
+ export type AdmissionStatus = 'admitted' | 'denied' | 'unconstrained' | 'indeterminate';
29
+ export type AdmissionMode = 'engines' | 'peers' | 'none';
30
+ export interface AdmissionResult {
31
+ readonly status: AdmissionStatus;
32
+ readonly mode: AdmissionMode;
33
+ /** Every requirement that was actually evaluated (empty for `none`). */
34
+ readonly requirements: readonly Requirement[];
35
+ /** Subset of `requirements` with `satisfied === false`. */
36
+ readonly unmet: readonly Requirement[];
37
+ /** Set when the runtime version is unknown, making admission unknowable. */
38
+ readonly indeterminateReason: string | null;
39
+ }
40
+ /** True for the peer package names that participate in admission. */
41
+ export declare function isAdmissionPeer(key: string): boolean;
42
+ interface EvaluatedRange {
43
+ readonly satisfied: boolean;
44
+ readonly failure: RequirementFailure | null;
45
+ }
46
+ /**
47
+ * Evaluate one declared range against one runtime version.
48
+ * Never throws: invalid input becomes a `satisfied: false` verdict.
49
+ */
50
+ export declare function evaluateRange(range: unknown, runtime: string): EvaluatedRange;
51
+ interface EnginesReading {
52
+ readonly kind: 'absent' | 'malformed' | 'range';
53
+ readonly value: string;
54
+ }
55
+ /** Read `engines.dsh` with the same tolerance as the upstream manifest reader. */
56
+ export declare function readEnginesDsh(manifest: unknown): EnginesReading;
57
+ /**
58
+ * Compute the admission decision for one plugin manifest on one runtime.
59
+ *
60
+ * Deterministic and side-effect free: same inputs always produce the same output,
61
+ * with requirements in stable order so stdout can be diffed.
62
+ */
63
+ export declare function evaluateAdmission(manifest: unknown, runtime: string | null): AdmissionResult;
64
+ export {};
65
+ //# sourceMappingURL=admission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admission.d.ts","sourceRoot":"","sources":["../src/admission.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,0EAA0E;AAC1E,MAAM,MAAM,kBAAkB,GAC1B,aAAa,GACb,eAAe,GACf,oBAAoB,GACpB,aAAa,GACb,oBAAoB,CAAA;AAExB,MAAM,WAAW,WAAW;IAC1B,gEAAgE;IAChE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAA;CAC5C;AAED,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,QAAQ,GAAG,eAAe,GAAG,eAAe,CAAA;AAEvF,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAA;AAExD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAA;IAChC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;IAC5B,wEAAwE;IACxE,QAAQ,CAAC,YAAY,EAAE,SAAS,WAAW,EAAE,CAAA;IAC7C,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAA;IACtC,4EAA4E;IAC5E,QAAQ,CAAC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5C;AAKD,qEAAqE;AACrE,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,UAAU,cAAc;IACtB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAA;CAC5C;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,cAAc,CAgB7E;AAED,UAAU,cAAc;IACtB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAA;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,OAAO,GAAG,cAAc,CAmBhE;AAiBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,eAAe,CAqE5F"}
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Mirrors DeepSeek Harness admission semantics.
3
+ *
4
+ * SOURCE OF TRUTH (do not "improve" these rules from memory):
5
+ * `@deepseek-ai/dsh-app-boot@0.2.1-rc.2/README.md` — "Before a profile imports a
6
+ * plugin, DSH checks its declared DSH requirement against the single runtime
7
+ * version returned by `getDshRuntimeVersion()`":
8
+ *
9
+ * 1. `engines.dsh` present -> that range alone decides, peers are ignored.
10
+ * 2. otherwise -> every peer range on `@deepseek-ai/dsh` and
11
+ * `@deepseek-ai/dsh-*` must match (AND).
12
+ * 3. prereleases participate in range matching.
13
+ * 4. empty / invalid ranges (and `workspace:` in `engines.dsh`) are incompatible.
14
+ * 5. no declaration at all -> no constraint.
15
+ *
16
+ * Parity is locked by `data/gold/admission-cases.json` and enforced by
17
+ * `scripts/gate_admission_parity.mjs`. Change the rules there first.
18
+ */
19
+ import { satisfies, validRange } from 'semver';
20
+ const DSH_PEER_PREFIX = '@deepseek-ai/dsh-';
21
+ const DSH_PEER_EXACT = '@deepseek-ai/dsh';
22
+ /** True for the peer package names that participate in admission. */
23
+ export function isAdmissionPeer(key) {
24
+ return key === DSH_PEER_EXACT || key.startsWith(DSH_PEER_PREFIX);
25
+ }
26
+ /**
27
+ * Evaluate one declared range against one runtime version.
28
+ * Never throws: invalid input becomes a `satisfied: false` verdict.
29
+ */
30
+ export function evaluateRange(range, runtime) {
31
+ if (typeof range !== 'string') {
32
+ return { satisfied: false, failure: 'malformed-manifest' };
33
+ }
34
+ const trimmed = range.trim();
35
+ // semver treats '' as '*', but DSH treats an empty requirement as incompatible.
36
+ if (trimmed === '') {
37
+ return { satisfied: false, failure: 'empty-range' };
38
+ }
39
+ if (trimmed.startsWith('workspace:')) {
40
+ return { satisfied: false, failure: 'workspace-protocol' };
41
+ }
42
+ if (validRange(trimmed, { includePrerelease: true }) === null) {
43
+ return { satisfied: false, failure: 'invalid-range' };
44
+ }
45
+ return { satisfied: satisfies(runtime, trimmed, { includePrerelease: true }), failure: null };
46
+ }
47
+ /** Read `engines.dsh` with the same tolerance as the upstream manifest reader. */
48
+ export function readEnginesDsh(manifest) {
49
+ if (manifest === null || typeof manifest !== 'object') {
50
+ return { kind: 'malformed', value: '' };
51
+ }
52
+ const engines = manifest.engines;
53
+ if (engines === undefined) {
54
+ return { kind: 'absent', value: '' };
55
+ }
56
+ if (engines === null || typeof engines !== 'object') {
57
+ return { kind: 'malformed', value: '' };
58
+ }
59
+ const dsh = engines.dsh;
60
+ if (dsh === undefined) {
61
+ return { kind: 'absent', value: '' };
62
+ }
63
+ if (typeof dsh !== 'string') {
64
+ return { kind: 'malformed', value: '' };
65
+ }
66
+ return { kind: 'range', value: dsh };
67
+ }
68
+ /** Peer package names that participate in admission, in stable key order. */
69
+ function peerRequirements(manifest) {
70
+ if (manifest === null || typeof manifest !== 'object') {
71
+ return [];
72
+ }
73
+ const peers = manifest.peerDependencies;
74
+ if (peers === null || typeof peers !== 'object') {
75
+ return [];
76
+ }
77
+ return Object.entries(peers)
78
+ .filter(([key]) => isAdmissionPeer(key))
79
+ .sort(([a], [b]) => a.localeCompare(b))
80
+ .map(([key, value]) => ({ key, range: typeof value === 'string' ? value : String(value) }));
81
+ }
82
+ /**
83
+ * Compute the admission decision for one plugin manifest on one runtime.
84
+ *
85
+ * Deterministic and side-effect free: same inputs always produce the same output,
86
+ * with requirements in stable order so stdout can be diffed.
87
+ */
88
+ export function evaluateAdmission(manifest, runtime) {
89
+ if (runtime === null || runtime.trim() === '') {
90
+ return {
91
+ status: 'indeterminate',
92
+ mode: 'none',
93
+ requirements: [],
94
+ unmet: [],
95
+ indeterminateReason: 'runtime-version-unknown',
96
+ };
97
+ }
98
+ const engines = readEnginesDsh(manifest);
99
+ if (engines.kind === 'malformed') {
100
+ const requirement = {
101
+ key: 'dsh',
102
+ range: '<malformed>',
103
+ satisfied: false,
104
+ failure: 'malformed-manifest',
105
+ };
106
+ return {
107
+ status: 'denied',
108
+ mode: 'engines',
109
+ requirements: [requirement],
110
+ unmet: [requirement],
111
+ indeterminateReason: null,
112
+ };
113
+ }
114
+ if (engines.kind === 'range') {
115
+ const evaluated = evaluateRange(engines.value, runtime);
116
+ const requirement = {
117
+ key: 'dsh',
118
+ range: engines.value,
119
+ satisfied: evaluated.satisfied,
120
+ failure: evaluated.failure,
121
+ };
122
+ return {
123
+ status: evaluated.satisfied ? 'admitted' : 'denied',
124
+ mode: 'engines',
125
+ requirements: [requirement],
126
+ unmet: evaluated.satisfied ? [] : [requirement],
127
+ indeterminateReason: null,
128
+ };
129
+ }
130
+ const peerKeys = peerRequirements(manifest);
131
+ if (peerKeys.length === 0) {
132
+ return {
133
+ status: 'unconstrained',
134
+ mode: 'none',
135
+ requirements: [],
136
+ unmet: [],
137
+ indeterminateReason: null,
138
+ };
139
+ }
140
+ const requirements = peerKeys.map(({ key, range }) => {
141
+ const evaluated = evaluateRange(range, runtime);
142
+ return { key, range, satisfied: evaluated.satisfied, failure: evaluated.failure };
143
+ });
144
+ const unmet = requirements.filter((requirement) => !requirement.satisfied);
145
+ return {
146
+ status: unmet.length === 0 ? 'admitted' : 'denied',
147
+ mode: 'peers',
148
+ requirements,
149
+ unmet,
150
+ indeterminateReason: null,
151
+ };
152
+ }
153
+ //# sourceMappingURL=admission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admission.js","sourceRoot":"","sources":["../src/admission.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Capability layer (M2-a · T-202, narrowed).
3
+ *
4
+ * WHY THIS IS FACTS AND DIFFS, NOT "MISSING TOOL" VERDICTS: upstream plugins do not
5
+ * declare their tool names anywhere static. `@deepseek-ai/dsh-tool-bash` has no `dsh`
6
+ * field in `package.json`; the only place `bash` appears is `defineTool({ name: 'bash' })`
7
+ * inside the built `lib/index.js`. So "this plugin declared tool X and X is not
8
+ * registered" cannot be derived — it would require us to ship a hand-written
9
+ * plugin→tool mapping table, which is exactly the guessing this project forbids
10
+ * (`PROJECT_RULES.md`, no evidence → `unknown`).
11
+ *
12
+ * What *is* real:
13
+ * - the registry's own list of tools, read from `ctx.tools.schemas()` in the Host;
14
+ * - the difference between two samples, which answers "this row disappearing removed
15
+ * which abilities" — the primitive a bisect (T-114) needs.
16
+ *
17
+ * Everything below is pure: parsing is defensive, the diff is deterministic, and no
18
+ * conclusion is invented from an absence.
19
+ */
20
+ import { type CoreError, type Result } from './result.js';
21
+ import { type InventoryTransport } from './inventory.js';
22
+ export interface CapabilityTool {
23
+ /** Model-facing tool name, exactly as registered. */
24
+ readonly name: string;
25
+ /** Optional one-line description; absent when the deployment does not publish one. */
26
+ readonly description: string | null;
27
+ }
28
+ export interface CapabilitySnapshot {
29
+ /**
30
+ * `'tools'` when a tool registry answered; `null` when the deployment composed none.
31
+ * The distinction matters: "no registry" is not "no tools", and reporting an empty
32
+ * list for both would turn a missing service into a clean bill.
33
+ */
34
+ readonly registry: 'tools' | null;
35
+ readonly tools: readonly CapabilityTool[];
36
+ readonly sampledAt: string;
37
+ /** How the list was read; same contract as the phase layer. */
38
+ readonly transport: InventoryTransport | null;
39
+ /** Channels this build can see but does not read yet. Surfaced, never judged. */
40
+ readonly unjudged: readonly string[];
41
+ /** Why the registry read failed, when it did — never conflated with an empty list. */
42
+ readonly readError: string | null;
43
+ /** Which presentation the list was read under (`ptc` collapses to `run_code`). */
44
+ readonly presentation: string | null;
45
+ /** Why forcing the native view failed, when it did. */
46
+ readonly presentationError: string | null;
47
+ }
48
+ export interface CapabilityMeta {
49
+ readonly sampledAt: string;
50
+ }
51
+ /** Parse one `capabilities` block defensively; a renamed field must fail loudly. */
52
+ export declare function parseCapabilitySnapshot(value: unknown, meta: CapabilityMeta): Result<CapabilitySnapshot, CoreError>;
53
+ export interface CapabilityDiff {
54
+ /** Present before, absent after. */
55
+ readonly removed: readonly string[];
56
+ /** Absent before, present after. */
57
+ readonly added: readonly string[];
58
+ /** Present in both; carried so a reader can see the comparison was not empty. */
59
+ readonly unchanged: number;
60
+ }
61
+ /**
62
+ * What changed between two capability samples.
63
+ *
64
+ * This is deliberately not a judgement: a tool disappearing after a row was disabled is
65
+ * the causal link the operator asked for, but the same diff means "an unrelated plugin
66
+ * crashed" if nobody touched a row. The caller records which experiment produced it.
67
+ */
68
+ export declare function diffCapabilities(before: CapabilitySnapshot, after: CapabilitySnapshot): CapabilityDiff;
69
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,MAAM,EAAW,MAAM,aAAa,CAAA;AAClE,OAAO,EAAE,KAAK,kBAAkB,EAAsB,MAAM,gBAAgB,CAAA;AAE5E,MAAM,WAAW,cAAc;IAC7B,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,sFAAsF;IACtF,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CACpC;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAA;IACjC,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAA;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,EAAE,kBAAkB,GAAG,IAAI,CAAA;IAC7C,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,sFAAsF;IACtF,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,kFAAkF;IAClF,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IACpC,uDAAuD;IACvD,QAAQ,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1C;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAID,oFAAoF;AACpF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,GAAG,MAAM,CAAC,kBAAkB,EAAE,SAAS,CAAC,CAiDnH;AAED,MAAM,WAAW,cAAc;IAC7B,oCAAoC;IACpC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,oCAAoC;IACpC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,iFAAiF;IACjF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,kBAAkB,GACxB,cAAc,CAOhB"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Capability layer (M2-a · T-202, narrowed).
3
+ *
4
+ * WHY THIS IS FACTS AND DIFFS, NOT "MISSING TOOL" VERDICTS: upstream plugins do not
5
+ * declare their tool names anywhere static. `@deepseek-ai/dsh-tool-bash` has no `dsh`
6
+ * field in `package.json`; the only place `bash` appears is `defineTool({ name: 'bash' })`
7
+ * inside the built `lib/index.js`. So "this plugin declared tool X and X is not
8
+ * registered" cannot be derived — it would require us to ship a hand-written
9
+ * plugin→tool mapping table, which is exactly the guessing this project forbids
10
+ * (`PROJECT_RULES.md`, no evidence → `unknown`).
11
+ *
12
+ * What *is* real:
13
+ * - the registry's own list of tools, read from `ctx.tools.schemas()` in the Host;
14
+ * - the difference between two samples, which answers "this row disappearing removed
15
+ * which abilities" — the primitive a bisect (T-114) needs.
16
+ *
17
+ * Everything below is pure: parsing is defensive, the diff is deterministic, and no
18
+ * conclusion is invented from an absence.
19
+ */
20
+ import { err, ok } from './result.js';
21
+ import { readTransportValue } from './inventory.js';
22
+ const KNOWN_KEYS = new Set(['registry', 'tools', 'sampled_at', 'sampledAt', 'transport', 'unjudged', 'note', 'read_error', 'presentation', 'presentation_error']);
23
+ /** Parse one `capabilities` block defensively; a renamed field must fail loudly. */
24
+ export function parseCapabilitySnapshot(value, meta) {
25
+ const invalid = (detail) => err({ code: 'capabilities-invalid', detail });
26
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
27
+ return invalid('capabilities is not an object');
28
+ }
29
+ const record = value;
30
+ const registryRaw = record['registry'];
31
+ if (registryRaw !== null && registryRaw !== 'tools' && registryRaw !== undefined) {
32
+ return invalid(`registry is ${JSON.stringify(registryRaw)}, expected "tools" or null`);
33
+ }
34
+ const registry = registryRaw === undefined ? null : registryRaw;
35
+ const rawTools = record['tools'];
36
+ if (!Array.isArray(rawTools))
37
+ return invalid('tools is missing or is not an array');
38
+ const tools = [];
39
+ for (const [index, candidate] of rawTools.entries()) {
40
+ if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate)) {
41
+ return invalid(`tools[${index}] is not an object`);
42
+ }
43
+ const tool = candidate;
44
+ const name = typeof tool['name'] === 'string' ? tool['name'].trim() : '';
45
+ if (name === '')
46
+ return invalid(`tools[${index}] has no name`);
47
+ const description = typeof tool['description'] === 'string' && tool['description'] !== '' ? tool['description'] : null;
48
+ tools.push({ name, description });
49
+ }
50
+ // A registry that answered with no tools is legal; a registry that answered with a
51
+ // duplicate name is not (the Host rejects duplicate registrations, so this means the
52
+ // sample was assembled by hand).
53
+ const seen = new Set();
54
+ for (const tool of tools) {
55
+ if (seen.has(tool.name))
56
+ return invalid(`tool "${tool.name}" appears twice in one snapshot`);
57
+ seen.add(tool.name);
58
+ }
59
+ const transport = readTransportValue(record['transport']);
60
+ if (transport === 'invalid')
61
+ return invalid(`transport is ${JSON.stringify(record['transport'])}`);
62
+ const unjudged = Array.isArray(record['unjudged'])
63
+ ? record['unjudged'].filter((entry) => typeof entry === 'string').sort()
64
+ : [];
65
+ void KNOWN_KEYS;
66
+ const sampledAt = typeof record['sampled_at'] === 'string' ? record['sampled_at'] : typeof record['sampledAt'] === 'string' ? record['sampledAt'] : meta.sampledAt;
67
+ return ok({
68
+ registry,
69
+ tools: [...tools].sort((left, right) => left.name.localeCompare(right.name)),
70
+ sampledAt,
71
+ transport,
72
+ unjudged,
73
+ readError: typeof record['read_error'] === 'string' ? record['read_error'] : null,
74
+ presentation: typeof record['presentation'] === 'string' ? record['presentation'] : null,
75
+ presentationError: typeof record['presentation_error'] === 'string' ? record['presentation_error'] : null,
76
+ });
77
+ }
78
+ /**
79
+ * What changed between two capability samples.
80
+ *
81
+ * This is deliberately not a judgement: a tool disappearing after a row was disabled is
82
+ * the causal link the operator asked for, but the same diff means "an unrelated plugin
83
+ * crashed" if nobody touched a row. The caller records which experiment produced it.
84
+ */
85
+ export function diffCapabilities(before, after) {
86
+ const beforeNames = new Set(before.tools.map((tool) => tool.name));
87
+ const afterNames = new Set(after.tools.map((tool) => tool.name));
88
+ const removed = [...beforeNames].filter((name) => !afterNames.has(name)).sort();
89
+ const added = [...afterNames].filter((name) => !beforeNames.has(name)).sort();
90
+ const unchanged = [...beforeNames].filter((name) => afterNames.has(name)).length;
91
+ return { removed, added, unchanged };
92
+ }
93
+ //# sourceMappingURL=capabilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `dsh.compatibility` — the declaration channel we did not read (BS-13 / T-108).
3
+ *
4
+ * WHY THIS FILE EXISTS (CASE-7590): the case was scored as a miss because neither
5
+ * plugin declares an `@deepseek-ai/dsh*` peer range. The R6 adversary pass showed
6
+ * "declares nothing" was too strong: `@xmanrui/dsh-im` carries
7
+ *
8
+ * "dsh": { "compatibility": { "dsh": "0.1.7-alpha.1",
9
+ * "dshReleases": { "0.1.7-alpha.1": "compatible" } } }
10
+ *
11
+ * while the profile runs `0.1.7-alpha.2`. The information was in the manifest all
12
+ * along; the engine only ever looked at `engines.dsh` and peers.
13
+ *
14
+ * HARD RULE (tier discipline): this channel never decides admission. It is
15
+ * `declared`-tier **secondary** evidence that gets listed next to the admission
16
+ * conclusion. It can never produce `blocked` on its own, and it can never upgrade
17
+ * an `unknown` to `go` — the author's self-declaration is not a measurement.
18
+ *
19
+ * Parsing is defensive: unknown shapes degrade to `null` (no channel), never to a
20
+ * guess. `evaluateRange` is reused from `admission.ts` so that prerelease and
21
+ * empty/invalid-range semantics stay byte-identical with the admission engine.
22
+ */
23
+ /** One author-published per-release statement (`dsh.compatibility.dshReleases`). */
24
+ export interface CompatRelease {
25
+ readonly version: string;
26
+ readonly status: string;
27
+ }
28
+ /** The parsed channel, flattened enough to be rendered without guessing. */
29
+ export interface CompatChannel {
30
+ /** One-line summary a user can act on, e.g. `dsh 0.1.7-alpha.1`. */
31
+ readonly claim: string;
32
+ /** `dsh.compatibility.dsh`, verbatim (may be a version or a range). */
33
+ readonly pinned: string | null;
34
+ /** `dsh.compatibility.dshReleases`, sorted by version. */
35
+ readonly releases: readonly CompatRelease[];
36
+ /** `dsh.compatibility.profiles`, sorted. */
37
+ readonly profiles: readonly string[];
38
+ }
39
+ /** What the author's own channel says about the running build. */
40
+ export type CompatStanding = 'match' | 'mismatch' | 'unknown';
41
+ export interface CompatReading {
42
+ readonly channel: CompatChannel;
43
+ readonly standing: CompatStanding;
44
+ }
45
+ /**
46
+ * Parse `dsh.compatibility` out of a plugin manifest.
47
+ *
48
+ * Returns `null` when the manifest says nothing in this channel — the absence is
49
+ * reported as "no secondary evidence", never as "compatible".
50
+ */
51
+ export declare function readCompatChannel(manifest: unknown): CompatChannel | null;
52
+ /**
53
+ * Judge the channel against the running build.
54
+ *
55
+ * Order of authority inside the channel: an exact `dshReleases` entry for this
56
+ * runtime beats the broader `dsh` statement; `profiles` alone can never judge a
57
+ * version, so it stays `unknown`.
58
+ */
59
+ export declare function judgeCompatChannel(manifest: unknown, runtime: string | null): CompatReading | null;
60
+ //# sourceMappingURL=compat-channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compat-channel.d.ts","sourceRoot":"","sources":["../src/compat-channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAMH,oFAAoF;AACpF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,uEAAuE;IACvE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAA;IAC3C,4CAA4C;IAC5C,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CACrC;AAgBD,kEAAkE;AAClE,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,CAAA;AAE7D,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAA;IAC/B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAA;CAClC;AAWD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CA+BzE;AAUD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,aAAa,GAAG,IAAI,CAqBlG"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * `dsh.compatibility` — the declaration channel we did not read (BS-13 / T-108).
3
+ *
4
+ * WHY THIS FILE EXISTS (CASE-7590): the case was scored as a miss because neither
5
+ * plugin declares an `@deepseek-ai/dsh*` peer range. The R6 adversary pass showed
6
+ * "declares nothing" was too strong: `@xmanrui/dsh-im` carries
7
+ *
8
+ * "dsh": { "compatibility": { "dsh": "0.1.7-alpha.1",
9
+ * "dshReleases": { "0.1.7-alpha.1": "compatible" } } }
10
+ *
11
+ * while the profile runs `0.1.7-alpha.2`. The information was in the manifest all
12
+ * along; the engine only ever looked at `engines.dsh` and peers.
13
+ *
14
+ * HARD RULE (tier discipline): this channel never decides admission. It is
15
+ * `declared`-tier **secondary** evidence that gets listed next to the admission
16
+ * conclusion. It can never produce `blocked` on its own, and it can never upgrade
17
+ * an `unknown` to `go` — the author's self-declaration is not a measurement.
18
+ *
19
+ * Parsing is defensive: unknown shapes degrade to `null` (no channel), never to a
20
+ * guess. `evaluateRange` is reused from `admission.ts` so that prerelease and
21
+ * empty/invalid-range semantics stay byte-identical with the admission engine.
22
+ */
23
+ import { valid } from 'semver';
24
+ import { evaluateRange } from './admission.js';
25
+ /**
26
+ * Compare keys the way an author would write them, not byte-for-byte.
27
+ *
28
+ * WHY (R9 adversary finding): `dshReleases` is a hand-written object in someone's
29
+ * package.json, so `v0.1.7-alpha.2` or `" 0.1.7-alpha.2 "` happen. A byte compare
30
+ * missed the entry, fell through to a broader `dsh: "*"` statement, and reported a
31
+ * *match* for a build the author had explicitly marked incompatible — the one
32
+ * direction this channel must never get wrong.
33
+ */
34
+ function normalizeVersion(value) {
35
+ const trimmed = value.trim().replace(/^[=v]+/i, '');
36
+ return valid(trimmed) ?? trimmed;
37
+ }
38
+ /** Release statuses that mean "the author says this build is fine". */
39
+ const COMPATIBLE_STATUS = new Set(['compatible', 'ok', 'supported', 'yes', 'true']);
40
+ function asRecord(value) {
41
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
42
+ ? value
43
+ : null;
44
+ }
45
+ /**
46
+ * Parse `dsh.compatibility` out of a plugin manifest.
47
+ *
48
+ * Returns `null` when the manifest says nothing in this channel — the absence is
49
+ * reported as "no secondary evidence", never as "compatible".
50
+ */
51
+ export function readCompatChannel(manifest) {
52
+ const dsh = asRecord(asRecord(manifest)?.['dsh']);
53
+ const compatibility = asRecord(dsh?.['compatibility']);
54
+ if (compatibility === null)
55
+ return null;
56
+ const rawPinned = compatibility['dsh'];
57
+ const pinned = typeof rawPinned === 'string' && rawPinned.trim() !== '' ? rawPinned.trim() : null;
58
+ const releases = [];
59
+ const rawReleases = asRecord(compatibility['dshReleases']);
60
+ if (rawReleases !== null) {
61
+ for (const [version, value] of Object.entries(rawReleases)) {
62
+ if (typeof value === 'string' && value.trim() !== '') {
63
+ releases.push({ version, status: value.trim() });
64
+ }
65
+ }
66
+ releases.sort((a, b) => a.version.localeCompare(b.version));
67
+ }
68
+ const profiles = [];
69
+ const rawProfiles = compatibility['profiles'];
70
+ if (Array.isArray(rawProfiles)) {
71
+ for (const value of rawProfiles) {
72
+ if (typeof value === 'string' && value.trim() !== '')
73
+ profiles.push(value.trim());
74
+ }
75
+ profiles.sort((a, b) => a.localeCompare(b));
76
+ }
77
+ if (pinned === null && releases.length === 0 && profiles.length === 0)
78
+ return null;
79
+ return { claim: claimOf(pinned, releases, profiles), pinned, releases, profiles };
80
+ }
81
+ function claimOf(pinned, releases, profiles) {
82
+ if (pinned !== null)
83
+ return `dsh ${pinned}`;
84
+ if (releases.length > 0) {
85
+ return `dshReleases ${releases.map((release) => `${release.version}: ${release.status}`).join(', ')}`;
86
+ }
87
+ return `profiles ${profiles.join(', ')}`;
88
+ }
89
+ /**
90
+ * Judge the channel against the running build.
91
+ *
92
+ * Order of authority inside the channel: an exact `dshReleases` entry for this
93
+ * runtime beats the broader `dsh` statement; `profiles` alone can never judge a
94
+ * version, so it stays `unknown`.
95
+ */
96
+ export function judgeCompatChannel(manifest, runtime) {
97
+ const channel = readCompatChannel(manifest);
98
+ if (channel === null)
99
+ return null;
100
+ if (runtime === null || runtime.trim() === '')
101
+ return { channel, standing: 'unknown' };
102
+ const wanted = normalizeVersion(runtime);
103
+ const exact = channel.releases.find((release) => normalizeVersion(release.version) === wanted);
104
+ if (exact !== undefined) {
105
+ const status = exact.status.toLowerCase();
106
+ const compatible = COMPATIBLE_STATUS.has(status);
107
+ // Anything the author wrote that is not a known "yes" is not a "yes": the
108
+ // conservative reading is exactly what keeps this channel from ever blocking.
109
+ return { channel, standing: compatible ? 'match' : 'mismatch' };
110
+ }
111
+ if (channel.pinned !== null) {
112
+ const evaluated = evaluateRange(channel.pinned, runtime);
113
+ return { channel, standing: evaluated.satisfied ? 'match' : 'mismatch' };
114
+ }
115
+ return { channel, standing: 'unknown' };
116
+ }
117
+ //# sourceMappingURL=compat-channel.js.map